├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src └── lib.rs ├── static_sqlite_async ├── .gitignore ├── Cargo.toml └── src │ └── lib.rs ├── static_sqlite_core ├── .gitignore ├── Cargo.toml └── src │ ├── ffi.rs │ └── lib.rs ├── static_sqlite_ffi ├── Cargo.toml ├── build.rs ├── sqlite3.h ├── src │ ├── bindings.rs │ └── lib.rs └── wrapper.h ├── static_sqlite_macros ├── Cargo.lock ├── Cargo.toml └── src │ ├── errors.rs │ ├── lib.rs │ ├── names.rs │ └── schema.rs └── tests ├── integration_test.rs └── ui ├── fk_fails.rs └── fk_fails.stderr /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | target 3 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aho-corasick" 22 | version = "1.1.3" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 25 | dependencies = [ 26 | "memchr", 27 | ] 28 | 29 | [[package]] 30 | name = "backtrace" 31 | version = "0.3.74" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 34 | dependencies = [ 35 | "addr2line", 36 | "cfg-if", 37 | "libc", 38 | "miniz_oxide", 39 | "object", 40 | "rustc-demangle", 41 | "windows-targets", 42 | ] 43 | 44 | [[package]] 45 | name = "bindgen" 46 | version = "0.71.1" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" 49 | dependencies = [ 50 | "bitflags", 51 | "cexpr", 52 | "clang-sys", 53 | "itertools", 54 | "log", 55 | "prettyplease", 56 | "proc-macro2", 57 | "quote", 58 | "regex", 59 | "rustc-hash", 60 | "shlex", 61 | "syn", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.6.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 69 | 70 | [[package]] 71 | name = "cexpr" 72 | version = "0.6.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 75 | dependencies = [ 76 | "nom", 77 | ] 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "clang-sys" 87 | version = "1.8.1" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 90 | dependencies = [ 91 | "glob", 92 | "libc", 93 | "libloading", 94 | ] 95 | 96 | [[package]] 97 | name = "crossbeam-channel" 98 | version = "0.5.14" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" 101 | dependencies = [ 102 | "crossbeam-utils", 103 | ] 104 | 105 | [[package]] 106 | name = "crossbeam-utils" 107 | version = "0.8.21" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 110 | 111 | [[package]] 112 | name = "either" 113 | version = "1.13.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 116 | 117 | [[package]] 118 | name = "equivalent" 119 | version = "1.0.1" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 122 | 123 | [[package]] 124 | name = "gimli" 125 | version = "0.31.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" 128 | 129 | [[package]] 130 | name = "glob" 131 | version = "0.3.1" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 134 | 135 | [[package]] 136 | name = "hashbrown" 137 | version = "0.15.2" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 140 | 141 | [[package]] 142 | name = "indexmap" 143 | version = "2.7.0" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" 146 | dependencies = [ 147 | "equivalent", 148 | "hashbrown", 149 | ] 150 | 151 | [[package]] 152 | name = "itertools" 153 | version = "0.12.1" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 156 | dependencies = [ 157 | "either", 158 | ] 159 | 160 | [[package]] 161 | name = "itoa" 162 | version = "1.0.14" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 165 | 166 | [[package]] 167 | name = "libc" 168 | version = "0.2.169" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 171 | 172 | [[package]] 173 | name = "libloading" 174 | version = "0.8.6" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" 177 | dependencies = [ 178 | "cfg-if", 179 | "windows-targets", 180 | ] 181 | 182 | [[package]] 183 | name = "log" 184 | version = "0.4.22" 185 | source = "registry+https://github.com/rust-lang/crates.io-index" 186 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 187 | 188 | [[package]] 189 | name = "memchr" 190 | version = "2.7.4" 191 | source = "registry+https://github.com/rust-lang/crates.io-index" 192 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 193 | 194 | [[package]] 195 | name = "minimal-lexical" 196 | version = "0.2.1" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 199 | 200 | [[package]] 201 | name = "miniz_oxide" 202 | version = "0.8.0" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 205 | dependencies = [ 206 | "adler2", 207 | ] 208 | 209 | [[package]] 210 | name = "nom" 211 | version = "7.1.3" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 214 | dependencies = [ 215 | "memchr", 216 | "minimal-lexical", 217 | ] 218 | 219 | [[package]] 220 | name = "object" 221 | version = "0.36.4" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" 224 | dependencies = [ 225 | "memchr", 226 | ] 227 | 228 | [[package]] 229 | name = "pin-project-lite" 230 | version = "0.2.14" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 233 | 234 | [[package]] 235 | name = "prettyplease" 236 | version = "0.2.25" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" 239 | dependencies = [ 240 | "proc-macro2", 241 | "syn", 242 | ] 243 | 244 | [[package]] 245 | name = "proc-macro2" 246 | version = "1.0.92" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 249 | dependencies = [ 250 | "unicode-ident", 251 | ] 252 | 253 | [[package]] 254 | name = "quote" 255 | version = "1.0.37" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 258 | dependencies = [ 259 | "proc-macro2", 260 | ] 261 | 262 | [[package]] 263 | name = "regex" 264 | version = "1.11.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 267 | dependencies = [ 268 | "aho-corasick", 269 | "memchr", 270 | "regex-automata", 271 | "regex-syntax", 272 | ] 273 | 274 | [[package]] 275 | name = "regex-automata" 276 | version = "0.4.9" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 279 | dependencies = [ 280 | "aho-corasick", 281 | "memchr", 282 | "regex-syntax", 283 | ] 284 | 285 | [[package]] 286 | name = "regex-syntax" 287 | version = "0.8.5" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 290 | 291 | [[package]] 292 | name = "rustc-demangle" 293 | version = "0.1.24" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 296 | 297 | [[package]] 298 | name = "rustc-hash" 299 | version = "2.1.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" 302 | 303 | [[package]] 304 | name = "ryu" 305 | version = "1.0.18" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 308 | 309 | [[package]] 310 | name = "serde" 311 | version = "1.0.217" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 314 | dependencies = [ 315 | "serde_derive", 316 | ] 317 | 318 | [[package]] 319 | name = "serde_derive" 320 | version = "1.0.217" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 323 | dependencies = [ 324 | "proc-macro2", 325 | "quote", 326 | "syn", 327 | ] 328 | 329 | [[package]] 330 | name = "serde_json" 331 | version = "1.0.134" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" 334 | dependencies = [ 335 | "itoa", 336 | "memchr", 337 | "ryu", 338 | "serde", 339 | ] 340 | 341 | [[package]] 342 | name = "serde_spanned" 343 | version = "0.6.8" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 346 | dependencies = [ 347 | "serde", 348 | ] 349 | 350 | [[package]] 351 | name = "shlex" 352 | version = "1.3.0" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 355 | 356 | [[package]] 357 | name = "sqlparser" 358 | version = "0.52.0" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "9a875d8cd437cc8a97e9aeaeea352ec9a19aea99c23e9effb17757291de80b08" 361 | dependencies = [ 362 | "log", 363 | "sqlparser_derive", 364 | ] 365 | 366 | [[package]] 367 | name = "sqlparser_derive" 368 | version = "0.2.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" 371 | dependencies = [ 372 | "proc-macro2", 373 | "quote", 374 | "syn", 375 | ] 376 | 377 | [[package]] 378 | name = "static_sqlite" 379 | version = "0.1.0" 380 | dependencies = [ 381 | "static_sqlite_async", 382 | "static_sqlite_core", 383 | "static_sqlite_macros", 384 | "tokio", 385 | "trybuild", 386 | ] 387 | 388 | [[package]] 389 | name = "static_sqlite_async" 390 | version = "0.1.0" 391 | dependencies = [ 392 | "crossbeam-channel", 393 | "static_sqlite_core", 394 | "tokio", 395 | ] 396 | 397 | [[package]] 398 | name = "static_sqlite_core" 399 | version = "0.1.0" 400 | dependencies = [ 401 | "static_sqlite_ffi", 402 | "thiserror", 403 | ] 404 | 405 | [[package]] 406 | name = "static_sqlite_ffi" 407 | version = "0.1.0" 408 | dependencies = [ 409 | "bindgen", 410 | ] 411 | 412 | [[package]] 413 | name = "static_sqlite_macros" 414 | version = "0.1.0" 415 | dependencies = [ 416 | "proc-macro2", 417 | "quote", 418 | "sqlparser", 419 | "static_sqlite_core", 420 | "syn", 421 | ] 422 | 423 | [[package]] 424 | name = "syn" 425 | version = "2.0.92" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "70ae51629bf965c5c098cc9e87908a3df5301051a9e087d6f9bef5c9771ed126" 428 | dependencies = [ 429 | "proc-macro2", 430 | "quote", 431 | "unicode-ident", 432 | ] 433 | 434 | [[package]] 435 | name = "target-triple" 436 | version = "0.1.3" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" 439 | 440 | [[package]] 441 | name = "termcolor" 442 | version = "1.4.1" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 445 | dependencies = [ 446 | "winapi-util", 447 | ] 448 | 449 | [[package]] 450 | name = "thiserror" 451 | version = "1.0.63" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 454 | dependencies = [ 455 | "thiserror-impl", 456 | ] 457 | 458 | [[package]] 459 | name = "thiserror-impl" 460 | version = "1.0.63" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 463 | dependencies = [ 464 | "proc-macro2", 465 | "quote", 466 | "syn", 467 | ] 468 | 469 | [[package]] 470 | name = "tokio" 471 | version = "1.40.0" 472 | source = "registry+https://github.com/rust-lang/crates.io-index" 473 | checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" 474 | dependencies = [ 475 | "backtrace", 476 | "pin-project-lite", 477 | "tokio-macros", 478 | ] 479 | 480 | [[package]] 481 | name = "tokio-macros" 482 | version = "2.4.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 485 | dependencies = [ 486 | "proc-macro2", 487 | "quote", 488 | "syn", 489 | ] 490 | 491 | [[package]] 492 | name = "toml" 493 | version = "0.8.19" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 496 | dependencies = [ 497 | "serde", 498 | "serde_spanned", 499 | "toml_datetime", 500 | "toml_edit", 501 | ] 502 | 503 | [[package]] 504 | name = "toml_datetime" 505 | version = "0.6.8" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 508 | dependencies = [ 509 | "serde", 510 | ] 511 | 512 | [[package]] 513 | name = "toml_edit" 514 | version = "0.22.22" 515 | source = "registry+https://github.com/rust-lang/crates.io-index" 516 | checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" 517 | dependencies = [ 518 | "indexmap", 519 | "serde", 520 | "serde_spanned", 521 | "toml_datetime", 522 | "winnow", 523 | ] 524 | 525 | [[package]] 526 | name = "trybuild" 527 | version = "1.0.101" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" 530 | dependencies = [ 531 | "glob", 532 | "serde", 533 | "serde_derive", 534 | "serde_json", 535 | "target-triple", 536 | "termcolor", 537 | "toml", 538 | ] 539 | 540 | [[package]] 541 | name = "unicode-ident" 542 | version = "1.0.13" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 545 | 546 | [[package]] 547 | name = "winapi-util" 548 | version = "0.1.9" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 551 | dependencies = [ 552 | "windows-sys", 553 | ] 554 | 555 | [[package]] 556 | name = "windows-sys" 557 | version = "0.59.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 560 | dependencies = [ 561 | "windows-targets", 562 | ] 563 | 564 | [[package]] 565 | name = "windows-targets" 566 | version = "0.52.6" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 569 | dependencies = [ 570 | "windows_aarch64_gnullvm", 571 | "windows_aarch64_msvc", 572 | "windows_i686_gnu", 573 | "windows_i686_gnullvm", 574 | "windows_i686_msvc", 575 | "windows_x86_64_gnu", 576 | "windows_x86_64_gnullvm", 577 | "windows_x86_64_msvc", 578 | ] 579 | 580 | [[package]] 581 | name = "windows_aarch64_gnullvm" 582 | version = "0.52.6" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 585 | 586 | [[package]] 587 | name = "windows_aarch64_msvc" 588 | version = "0.52.6" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 591 | 592 | [[package]] 593 | name = "windows_i686_gnu" 594 | version = "0.52.6" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 597 | 598 | [[package]] 599 | name = "windows_i686_gnullvm" 600 | version = "0.52.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 603 | 604 | [[package]] 605 | name = "windows_i686_msvc" 606 | version = "0.52.6" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 609 | 610 | [[package]] 611 | name = "windows_x86_64_gnu" 612 | version = "0.52.6" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 615 | 616 | [[package]] 617 | name = "windows_x86_64_gnullvm" 618 | version = "0.52.6" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 621 | 622 | [[package]] 623 | name = "windows_x86_64_msvc" 624 | version = "0.52.6" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 627 | 628 | [[package]] 629 | name = "winnow" 630 | version = "0.6.20" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" 633 | dependencies = [ 634 | "memchr", 635 | ] 636 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_sqlite" 3 | version = "0.1.0" 4 | edition = "2021" 5 | resolver = "2" 6 | 7 | [dependencies] 8 | static_sqlite_macros = { path = "static_sqlite_macros", version = "0.1.0" } 9 | static_sqlite_core = { path = "static_sqlite_core", version = "0.1.0" } 10 | static_sqlite_async = { path = "static_sqlite_async", version = "0.1.0" } 11 | 12 | [dev-dependencies] 13 | tokio = { version = "1", features = ["rt", "sync", "macros"] } 14 | trybuild = "1.0" 15 | 16 | [workspace] 17 | members = ["static_sqlite_core", "static_sqlite_async", "static_sqlite_ffi"] 18 | 19 | [[test]] 20 | name = "integration_test" 21 | test = true 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # static_sqlite 2 | 3 | An easy way to map sql to rust functions and structs 4 | 5 | # Quickstart 6 | 7 | ```rust 8 | use static_sqlite::{sql, Result, self}; 9 | 10 | sql! { 11 | let migrate = r#" 12 | create table User ( 13 | id integer primary key, 14 | name text unique not null 15 | ); 16 | 17 | alter table User 18 | add column created_at integer; 19 | 20 | alter table User 21 | drop column created_at; 22 | "#; 23 | 24 | let insert_user = r#" 25 | insert into User (name) 26 | values (:name) 27 | returning * 28 | "#; 29 | } 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<()> { 33 | let db = static_sqlite::open("db.sqlite3").await?; 34 | let _ = migrate(&db).await?; 35 | let users = insert_user(&db, "swlkr").await?; 36 | let user = users.first().unwrap(); 37 | 38 | assert_eq!(user.id, 1); 39 | assert_eq!(user.name, "swlkr"); 40 | 41 | Ok(()) 42 | } 43 | ``` 44 | 45 | # Use 46 | 47 | ```sh 48 | cargo add --git https://github.com/swlkr/static_sqlite 49 | ``` 50 | 51 | # Treesitter 52 | 53 | ``` 54 | ((macro_invocation 55 | macro: 56 | [ 57 | (scoped_identifier 58 | name: (_) @_macro_name) 59 | (identifier) @_macro_name 60 | ] 61 | (token_tree 62 | (identifier) 63 | (raw_string_literal 64 | (string_content) @injection.content))) 65 | (#eq? @_macro_name "sql") 66 | (#set! injection.language "sql") 67 | (#set! injection.include-children)) 68 | ``` 69 | 70 | Happy hacking! 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate self as static_sqlite; 2 | pub use static_sqlite_async::{ 3 | execute, execute_all, open, query, rows, Error, FromRow, Result, Savepoint, Sqlite, Value, 4 | }; 5 | pub use static_sqlite_core::FirstRow; 6 | pub use static_sqlite_macros::sql; 7 | -------------------------------------------------------------------------------- /static_sqlite_async/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /static_sqlite_async/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_sqlite_async" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | static_sqlite_core = { path = "../static_sqlite_core", version = "0.1.0" } 8 | tokio = { version = "1", features = ["sync"] } 9 | crossbeam-channel = { version = "0.5" } 10 | -------------------------------------------------------------------------------- /static_sqlite_async/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Inspired by the incredible tokio-rusqlite crate 2 | // https://github.com/programatik29/tokio-rusqlite/blob/master/src/lib.rs 3 | 4 | use static_sqlite_core as core; 5 | use crossbeam_channel::Sender; 6 | use tokio::sync::oneshot; 7 | 8 | pub use static_sqlite_core::*; 9 | 10 | type CallFn = Box; 11 | 12 | enum Message { 13 | Execute(CallFn), 14 | Close(oneshot::Sender>), 15 | } 16 | 17 | #[derive(Clone)] 18 | pub struct Sqlite { 19 | sender: Sender, 20 | } 21 | 22 | impl Sqlite { 23 | pub async fn close(self) -> Result<()> { 24 | let (sender, receiver) = oneshot::channel::>(); 25 | 26 | if let Err(crossbeam_channel::SendError(_)) = self.sender.send(Message::Close(sender)) { 27 | return Ok(()); 28 | } 29 | 30 | let result = receiver.await; 31 | 32 | if result.is_err() { 33 | return Ok(()); 34 | } 35 | 36 | result 37 | .unwrap() 38 | .map_err(|e| Error::Sqlite(e.to_string())) 39 | } 40 | 41 | pub async fn call(&self, function: F) -> Result 42 | where 43 | F: FnOnce(&core::Sqlite) -> Result + 'static + Send, 44 | R: Send + 'static, 45 | { 46 | let (sender, receiver) = oneshot::channel::>(); 47 | 48 | self.sender 49 | .send(Message::Execute(Box::new(move |conn| { 50 | let value = function(conn); 51 | let _ = sender.send(value); 52 | }))) 53 | .map_err(|_| Error::ConnectionClosed)?; 54 | 55 | receiver.await.map_err(|_| Error::ConnectionClosed)? 56 | } 57 | } 58 | 59 | pub async fn open(path: impl ToString) -> Result { 60 | let path = path.to_string(); 61 | start(move || core::Sqlite::open(&path)).await 62 | } 63 | 64 | async fn start(open: F) -> Result 65 | where 66 | F: FnOnce() -> Result + Send + 'static, 67 | { 68 | let (sender, receiver) = crossbeam_channel::unbounded::(); 69 | let (result_sender, result_receiver) = oneshot::channel(); 70 | 71 | std::thread::spawn(move || { 72 | let mut conn = match open() { 73 | Ok(c) => c, 74 | Err(e) => { 75 | let _ = result_sender.send(Err(e)); 76 | return; 77 | } 78 | }; 79 | 80 | if let Err(_e) = result_sender.send(Ok(())) { 81 | return; 82 | } 83 | 84 | while let Ok(message) = receiver.recv() { 85 | match message { 86 | Message::Execute(f) => f(&mut conn), 87 | Message::Close(_s) => { 88 | todo!("Message::Close") 89 | // let result = drop(conn); 90 | 91 | // match result { 92 | // Ok(v) => { 93 | // s.send(Ok(v)).expect("failed to send message"); 94 | // break; 95 | // } 96 | // Err((c, e)) => { 97 | // conn = c; 98 | // s.send(Err(e)).expect("failed to receive message"); 99 | // } 100 | // } 101 | } 102 | } 103 | } 104 | }); 105 | 106 | result_receiver 107 | .await 108 | .expect("failed to receive message") 109 | .map(|_| Sqlite { sender }) 110 | } 111 | 112 | pub async fn execute(conn: &Sqlite, sql: String, params: Vec) -> Result { 113 | conn.call(move |conn| conn.execute(&sql, params)).await 114 | } 115 | 116 | pub async fn execute_all(conn: &Sqlite, sql: &'static str) -> Result<()> { 117 | let _ = conn.call(move |conn| conn.execute(sql, vec![])).await; 118 | Ok(()) 119 | } 120 | 121 | pub async fn query( 122 | conn: &Sqlite, 123 | sql: &'static str, 124 | params: Vec, 125 | ) -> Result> { 126 | conn.call(move |conn| conn.query(sql, ¶ms)).await 127 | } 128 | 129 | pub async fn rows( 130 | conn: Sqlite, 131 | sql: &'static str, 132 | params: &'static [Value], 133 | ) -> Result>> { 134 | conn.call(|conn| conn.rows(sql, params)).await 135 | } 136 | -------------------------------------------------------------------------------- /static_sqlite_core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /static_sqlite_core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_sqlite_core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | thiserror = "1" 8 | static_sqlite_ffi = { path = "../static_sqlite_ffi" } 9 | 10 | -------------------------------------------------------------------------------- /static_sqlite_core/src/ffi.rs: -------------------------------------------------------------------------------- 1 | use static_sqlite_ffi::{ 2 | sqlite3, sqlite3_bind_blob, sqlite3_bind_double, sqlite3_bind_int64, sqlite3_bind_null, 3 | sqlite3_bind_parameter_count, sqlite3_bind_parameter_name, sqlite3_bind_text, sqlite3_changes, 4 | sqlite3_close, sqlite3_column_bytes, sqlite3_column_count, sqlite3_column_double, 5 | sqlite3_column_int64, sqlite3_column_name, sqlite3_column_origin_name, 6 | sqlite3_column_table_name, sqlite3_column_text, sqlite3_column_type, sqlite3_errmsg, 7 | sqlite3_finalize, sqlite3_open, sqlite3_prepare_v2, sqlite3_step, sqlite3_stmt, 8 | }; 9 | 10 | use std::{ 11 | ffi::{c_char, c_int, CStr, CString, NulError}, 12 | num::TryFromIntError, 13 | ops::Deref, 14 | str::Utf8Error, 15 | }; 16 | 17 | const SQLITE_ROW: i32 = static_sqlite_ffi::SQLITE_ROW as i32; 18 | const SQLITE_DONE: i32 = static_sqlite_ffi::SQLITE_DONE as i32; 19 | 20 | #[derive(thiserror::Error, Debug)] 21 | pub enum Error { 22 | #[error("io error: {0}")] 23 | Io(#[from] std::io::Error), 24 | #[error("null error: {0}")] 25 | Null(#[from] NulError), 26 | #[error("cstring error: {0}")] 27 | TryFromInt(#[from] TryFromIntError), 28 | #[error("sqlite error: {0}")] 29 | Sqlite(String), 30 | #[error("UNIQUE constraint failed: {0}")] 31 | UniqueConstraint(String), 32 | #[error("sqlite file closed")] 33 | ConnectionClosed, 34 | #[error("sqlite row not found")] 35 | RowNotFound, 36 | #[error(transparent)] 37 | Utf8Error(#[from] Utf8Error), 38 | } 39 | 40 | pub type Result = std::result::Result; 41 | 42 | #[derive(Debug, Clone)] 43 | pub struct Sqlite { 44 | db: *mut static_sqlite_ffi::sqlite3, 45 | } 46 | 47 | unsafe impl Sync for Sqlite {} 48 | unsafe impl Send for Sqlite {} 49 | 50 | impl Sqlite { 51 | pub fn open(path: &str) -> Result { 52 | let c_path = CString::new(path)?; 53 | let mut db: *mut sqlite3 = core::ptr::null_mut(); 54 | 55 | unsafe { 56 | if sqlite3_open(c_path.as_ptr(), &mut db) != 0 { 57 | let error = CStr::from_ptr(static_sqlite_ffi::sqlite3_errmsg(db)) 58 | .to_string_lossy() 59 | .into_owned(); 60 | return Err(Error::Sqlite(error)); 61 | } 62 | } 63 | 64 | Ok(Sqlite { db }) 65 | } 66 | 67 | pub fn prepare(&self, sql: &str, params: &[Value]) -> Result<*mut sqlite3_stmt> { 68 | let c_sql = CString::new(sql)?; 69 | let mut stmt: *mut sqlite3_stmt = core::ptr::null_mut(); 70 | unsafe { 71 | if sqlite3_prepare_v2(self.db, c_sql.as_ptr(), -1, &mut stmt, std::ptr::null_mut()) != 0 72 | { 73 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 74 | .to_string_lossy() 75 | .into_owned(); 76 | return Err(Error::Sqlite(error)); 77 | } else { 78 | for (i, param) in params.iter().enumerate() { 79 | match param { 80 | Value::Text(s) => { 81 | let s = s.as_str(); 82 | sqlite3_bind_text( 83 | stmt, 84 | (i + 1) as i32, 85 | s.as_ptr() as *const _, 86 | s.len() as c_int, 87 | None, 88 | ); 89 | } 90 | Value::Integer(n) => { 91 | sqlite3_bind_int64(stmt, (i + 1) as i32, *n); 92 | } 93 | Value::Real(f) => { 94 | sqlite3_bind_double(stmt, (i + 1) as i32, *f); 95 | } 96 | Value::Blob(b) => { 97 | sqlite3_bind_blob( 98 | stmt, 99 | (i + 1) as i32, 100 | b.as_ptr() as *const _, 101 | b.len() as c_int, 102 | None, 103 | ); 104 | } 105 | Value::Null => { 106 | sqlite3_bind_null(stmt, (i + 1) as i32); 107 | } 108 | } 109 | } 110 | 111 | return Ok(stmt); 112 | } 113 | } 114 | } 115 | 116 | pub fn execute(&self, sql: &str, params: Vec) -> Result { 117 | unsafe { 118 | let stmt = self.prepare(&sql, ¶ms)?; 119 | 120 | loop { 121 | match sqlite3_step(stmt) { 122 | SQLITE_ROW | SQLITE_DONE => { 123 | break; 124 | } 125 | _ => { 126 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 127 | .to_string_lossy() 128 | .into_owned(); 129 | return Err(Error::Sqlite(error)); 130 | } 131 | } 132 | } 133 | 134 | if sqlite3_finalize(stmt) != 0 { 135 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 136 | .to_string_lossy() 137 | .into_owned(); 138 | return Err(Error::Sqlite(error)); 139 | } 140 | 141 | let changes = sqlite3_changes(self.db); 142 | Ok(changes) 143 | } 144 | } 145 | 146 | pub fn execute_all(&self, sql: &str) -> Result { 147 | self.execute(sql, vec![]) 148 | } 149 | 150 | pub fn query(&self, sql: &'static str, params: &[Value]) -> Result> { 151 | unsafe { 152 | let stmt = self.prepare(sql, params)?; 153 | let mut rows = Vec::new(); 154 | while sqlite3_step(stmt) == SQLITE_ROW { 155 | let column_count = sqlite3_column_count(stmt); 156 | let mut values: Vec<(String, Value)> = vec![]; 157 | 158 | for i in 0..column_count { 159 | let name = CStr::from_ptr(sqlite3_column_name(stmt, i)) 160 | .to_string_lossy() 161 | .into_owned(); 162 | 163 | let value = match sqlite3_column_type(stmt, i) { 164 | 1 => Value::Integer(sqlite3_column_int64(stmt, i)), 165 | 2 => Value::Real(sqlite3_column_double(stmt, i)), 166 | 3 => { 167 | let text = 168 | CStr::from_ptr(sqlite3_column_text(stmt, i) as *const c_char) 169 | .to_string_lossy() 170 | .into_owned(); 171 | Value::Text(text) 172 | } 173 | 4 => { 174 | let len = sqlite3_column_bytes(stmt, i) as usize; 175 | let ptr = sqlite3_column_text(stmt, i); 176 | let slice = std::slice::from_raw_parts(ptr, len); 177 | Value::Blob(slice.to_vec()) 178 | } 179 | _ => Value::Null, 180 | }; 181 | 182 | values.push((name, value)); 183 | } 184 | 185 | let row = T::from_row(values)?; 186 | rows.push(row); 187 | } 188 | 189 | if sqlite3_finalize(stmt) != 0 { 190 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 191 | .to_string_lossy() 192 | .into_owned(); 193 | if error.starts_with("UNIQUE constraint failed: ") { 194 | return Err(Error::UniqueConstraint( 195 | error.replace("UNIQUE constraint failed: ", ""), 196 | )); 197 | } else { 198 | return Err(Error::Sqlite(error)); 199 | } 200 | } 201 | 202 | Ok(rows) 203 | } 204 | } 205 | 206 | pub fn rows(&self, sql: &str, params: &[Value]) -> Result>> { 207 | unsafe { 208 | let stmt = self.prepare(sql, params)?; 209 | let mut rows = Vec::new(); 210 | while sqlite3_step(stmt) == SQLITE_ROW { 211 | let column_count = sqlite3_column_count(stmt); 212 | let mut values: Vec<(String, Value)> = vec![]; 213 | 214 | for i in 0..column_count { 215 | let name = CStr::from_ptr(sqlite3_column_name(stmt, i)) 216 | .to_string_lossy() 217 | .into_owned(); 218 | 219 | let value = match sqlite3_column_type(stmt, i) { 220 | 1 => Value::Integer(sqlite3_column_int64(stmt, i)), 221 | 2 => Value::Real(sqlite3_column_double(stmt, i)), 222 | 3 => { 223 | let text = 224 | CStr::from_ptr(sqlite3_column_text(stmt, i) as *const c_char) 225 | .to_string_lossy() 226 | .into_owned(); 227 | Value::Text(text) 228 | } 229 | 4 => { 230 | let len = sqlite3_column_bytes(stmt, i) as usize; 231 | let ptr = sqlite3_column_text(stmt, i); 232 | let slice = std::slice::from_raw_parts(ptr, len); 233 | Value::Blob(slice.to_vec()) 234 | } 235 | _ => Value::Null, 236 | }; 237 | 238 | values.push((name, value)); 239 | } 240 | 241 | rows.push(values); 242 | } 243 | 244 | if sqlite3_finalize(stmt) != 0 { 245 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 246 | .to_string_lossy() 247 | .into_owned(); 248 | if error.starts_with("UNIQUE constraint failed: ") { 249 | return Err(Error::UniqueConstraint( 250 | error.replace("UNIQUE constraint failed: ", ""), 251 | )); 252 | } else { 253 | return Err(Error::Sqlite(error)); 254 | } 255 | } 256 | 257 | Ok(rows) 258 | } 259 | } 260 | 261 | pub fn savepoint<'a>(&'a self, sqlite: &'a Sqlite, name: &'a str) -> Result> { 262 | Savepoint::new(sqlite, name) 263 | } 264 | 265 | pub fn column_names(&self, sql: &str) -> Result> { 266 | let mut columns = Vec::new(); 267 | unsafe { 268 | let stmt = self.prepare(sql, &[])?; 269 | let count = sqlite3_column_count(stmt); 270 | for i in 0..count { 271 | let name_ptr = sqlite3_column_origin_name(stmt, i); 272 | 273 | if !name_ptr.is_null() { 274 | let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); 275 | columns.push(name); 276 | } 277 | } 278 | } 279 | 280 | Ok(columns) 281 | } 282 | 283 | pub fn aliased_column_names(&self, sql: &str) -> Result> { 284 | let mut columns = Vec::new(); 285 | unsafe { 286 | let stmt = self.prepare(sql, &[])?; 287 | let count = sqlite3_column_count(stmt); 288 | for i in 0..count { 289 | let name_ptr = sqlite3_column_name(stmt, i); 290 | 291 | if !name_ptr.is_null() { 292 | let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); 293 | columns.push(name); 294 | } 295 | } 296 | } 297 | 298 | Ok(columns) 299 | } 300 | 301 | pub fn table_names(&self, sql: &str) -> Result> { 302 | let mut tables = Vec::new(); 303 | unsafe { 304 | let stmt = self.prepare(sql, &[])?; 305 | let count = sqlite3_column_count(stmt); 306 | for i in 0..count { 307 | let name_ptr = sqlite3_column_table_name(stmt, i); 308 | 309 | if !name_ptr.is_null() { 310 | let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); 311 | tables.push(name); 312 | } 313 | } 314 | } 315 | 316 | Ok(tables) 317 | } 318 | 319 | pub fn bind_param_names(&self, sql: &str) -> Result> { 320 | let mut params = Vec::new(); 321 | 322 | unsafe { 323 | let stmt = self.prepare(sql, &[])?; 324 | let param_count = sqlite3_bind_parameter_count(stmt); 325 | 326 | for i in 1..param_count + 1 { 327 | let name_ptr = sqlite3_bind_parameter_name(stmt, i); 328 | if !name_ptr.is_null() { 329 | let name = CStr::from_ptr(name_ptr).to_string_lossy().into_owned(); 330 | params.push(name); 331 | } 332 | } 333 | self.finalize(stmt)?; 334 | } 335 | 336 | Ok(params) 337 | } 338 | 339 | fn finalize(&self, stmt: *mut sqlite3_stmt) -> Result<()> { 340 | unsafe { 341 | if sqlite3_finalize(stmt) != 0 { 342 | let error = CStr::from_ptr(sqlite3_errmsg(self.db)) 343 | .to_string_lossy() 344 | .into_owned(); 345 | return Err(Error::Sqlite(error)); 346 | } 347 | } 348 | Ok(()) 349 | } 350 | } 351 | 352 | impl Drop for Sqlite { 353 | fn drop(&mut self) { 354 | unsafe { 355 | sqlite3_close(self.db); 356 | } 357 | } 358 | } 359 | 360 | #[derive(Debug)] 361 | pub struct Savepoint<'a> { 362 | sqlite: &'a Sqlite, 363 | name: &'a str, 364 | } 365 | 366 | impl<'a> Savepoint<'a> { 367 | pub fn new(sqlite: &'a Sqlite, name: &'a str) -> Result> { 368 | let sql = format!("savepoint {}", name); 369 | let _stmt = sqlite.prepare(&sql, &[])?; 370 | Ok(Self { sqlite, name }) 371 | } 372 | 373 | pub fn release(&self) -> Result<()> { 374 | let sql = format!("release savepoint {}", self.name); 375 | let _stmt = self.prepare(&sql, &[])?; 376 | Ok(()) 377 | } 378 | } 379 | 380 | impl<'a> Deref for Savepoint<'a> { 381 | type Target = Sqlite; 382 | 383 | fn deref(&self) -> &Self::Target { 384 | self.sqlite 385 | } 386 | } 387 | 388 | impl<'a> Drop for Savepoint<'a> { 389 | fn drop(&mut self) { 390 | self.release().expect("release savepoint failed") 391 | } 392 | } 393 | 394 | #[derive(Debug, Clone)] 395 | pub enum Value { 396 | Text(String), 397 | Integer(i64), 398 | Real(f64), 399 | Blob(Vec), 400 | Null, 401 | } 402 | 403 | #[derive(Debug, Clone)] 404 | pub enum DataType { 405 | Text, 406 | Integer, 407 | Real, 408 | Blob, 409 | Null, 410 | } 411 | 412 | pub trait FromRow: Sized { 413 | fn from_row(columns: Vec<(String, Value)>) -> Result; 414 | } 415 | 416 | impl TryFrom for String { 417 | type Error = Error; 418 | 419 | fn try_from(value: Value) -> std::result::Result { 420 | match value { 421 | Value::Text(s) => Ok(s), 422 | _ => Err(Error::Sqlite("column type mismatch".into())), 423 | } 424 | } 425 | } 426 | 427 | impl TryFrom for i64 { 428 | type Error = Error; 429 | 430 | fn try_from(value: Value) -> std::result::Result { 431 | match value { 432 | Value::Integer(val) => Ok(val), 433 | _ => Err(Error::Sqlite("column type mismatch".into())), 434 | } 435 | } 436 | } 437 | 438 | impl TryFrom for f64 { 439 | type Error = Error; 440 | 441 | fn try_from(value: Value) -> std::result::Result { 442 | match value { 443 | Value::Real(val) => Ok(val), 444 | _ => Err(Error::Sqlite("column type mismatch".into())), 445 | } 446 | } 447 | } 448 | 449 | impl TryFrom for Vec { 450 | type Error = Error; 451 | 452 | fn try_from(value: Value) -> std::result::Result { 453 | match value { 454 | Value::Blob(val) => Ok(val), 455 | _ => Err(Error::Sqlite("column type mismatch".into())), 456 | } 457 | } 458 | } 459 | 460 | impl TryFrom for Option { 461 | type Error = Error; 462 | 463 | fn try_from(value: Value) -> std::result::Result { 464 | match value { 465 | Value::Text(s) => Ok(Some(s)), 466 | Value::Null => Ok(None), 467 | _ => Err(Error::Sqlite("column type mismatch".into())), 468 | } 469 | } 470 | } 471 | impl TryFrom for Option { 472 | type Error = Error; 473 | 474 | fn try_from(value: Value) -> std::result::Result { 475 | match value { 476 | Value::Integer(val) => Ok(Some(val)), 477 | Value::Null => Ok(None), 478 | _ => Err(Error::Sqlite("column type mismatch".into())), 479 | } 480 | } 481 | } 482 | impl TryFrom for Option { 483 | type Error = Error; 484 | 485 | fn try_from(value: Value) -> std::result::Result { 486 | match value { 487 | Value::Real(val) => Ok(Some(val)), 488 | Value::Null => Ok(None), 489 | _ => Err(Error::Sqlite("column type mismatch".into())), 490 | } 491 | } 492 | } 493 | impl TryFrom for Option> { 494 | type Error = Error; 495 | 496 | fn try_from(value: Value) -> std::result::Result { 497 | match value { 498 | Value::Blob(val) => Ok(Some(val)), 499 | Value::Null => Ok(None), 500 | _ => Err(Error::Sqlite("column type mismatch".into())), 501 | } 502 | } 503 | } 504 | 505 | impl From<&str> for Value { 506 | fn from(value: &str) -> Self { 507 | Value::Text(value.into()) 508 | } 509 | } 510 | 511 | impl From for Value { 512 | fn from(value: String) -> Self { 513 | Value::Text(value) 514 | } 515 | } 516 | 517 | impl From> for Value { 518 | fn from(value: Option<&str>) -> Self { 519 | match value { 520 | Some(val) => Value::Text(val.into()), 521 | None => Value::Null, 522 | } 523 | } 524 | } 525 | 526 | impl From> for Value { 527 | fn from(value: Option) -> Self { 528 | match value { 529 | Some(val) => Value::Text(val), 530 | None => Value::Null, 531 | } 532 | } 533 | } 534 | 535 | impl From> for Value { 536 | fn from(value: Option) -> Self { 537 | match value { 538 | Some(val) => Value::Integer(val), 539 | None => Value::Null, 540 | } 541 | } 542 | } 543 | impl From for Value { 544 | fn from(value: i64) -> Self { 545 | Value::Integer(value) 546 | } 547 | } 548 | 549 | impl From> for Value { 550 | fn from(value: Option) -> Self { 551 | match value { 552 | Some(val) => Value::Real(val), 553 | None => Value::Null, 554 | } 555 | } 556 | } 557 | 558 | impl From for Value { 559 | fn from(value: f64) -> Self { 560 | Value::Real(value) 561 | } 562 | } 563 | 564 | impl From>> for Value { 565 | fn from(value: Option>) -> Self { 566 | match value { 567 | Some(val) => Value::Blob(val), 568 | None => Value::Null, 569 | } 570 | } 571 | } 572 | 573 | impl From> for Value { 574 | fn from(value: Vec) -> Self { 575 | Value::Blob(value) 576 | } 577 | } 578 | 579 | impl From<()> for Value { 580 | fn from(_value: ()) -> Self { 581 | Value::Null 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /static_sqlite_core/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod ffi; 2 | pub use ffi::{DataType, Error, FromRow, Result, Savepoint, Sqlite, Value}; 3 | 4 | pub fn open(path: &str) -> Result { 5 | Sqlite::open(path) 6 | } 7 | 8 | pub fn execute(conn: &Sqlite, sql: &str, params: Vec) -> Result { 9 | conn.execute(sql, params) 10 | } 11 | 12 | pub fn execute_all(conn: &Sqlite, sql: &str) -> Result { 13 | conn.execute(sql, vec![]) 14 | } 15 | 16 | pub fn query( 17 | conn: &Sqlite, 18 | sql: &'static str, 19 | params: &[Value], 20 | ) -> Result> { 21 | conn.query(sql, params) 22 | } 23 | 24 | pub fn rows(conn: &Sqlite, sql: &str, params: &[Value]) -> Result>> { 25 | conn.rows(sql, params) 26 | } 27 | 28 | pub fn savepoint<'a>(conn: &'a Sqlite, name: &'a str) -> Result> { 29 | conn.savepoint(conn, name) 30 | } 31 | 32 | pub(crate) fn user_version(db: &Sqlite) -> Result { 33 | let rws = rows(&db, "PRAGMA user_version", &[])?; 34 | match rws.into_iter().nth(0) { 35 | Some(cols) => match cols.into_iter().nth(0) { 36 | Some(pair) => pair.1.try_into(), 37 | None => Ok(0), 38 | }, 39 | None => Ok(0), 40 | } 41 | } 42 | 43 | pub(crate) fn set_user_version(db: &Sqlite, version: usize) -> Result<()> { 44 | let _ = execute(&db, &format!("PRAGMA user_version = {version}"), vec![])?; 45 | Ok(()) 46 | } 47 | 48 | pub fn migrate(db: &Sqlite, migrations: &[F]) -> Result<()> 49 | where 50 | F: Fn(&Sqlite) -> Result<()>, 51 | { 52 | let sp = savepoint(db, "migrate")?; 53 | let version = user_version(&sp)?; 54 | let pending_migrations = &migrations[(version as usize)..]; 55 | for migration in pending_migrations { 56 | migration(&sp)?; 57 | } 58 | set_user_version(&sp, migrations.len())?; 59 | 60 | Ok(()) 61 | } 62 | 63 | impl FromRow for () { 64 | fn from_row(_columns: Vec<(String, Value)>) -> Result { 65 | Ok(()) 66 | } 67 | } 68 | 69 | pub trait FirstRow 70 | where 71 | T: FromRow, 72 | { 73 | fn first_row(self) -> Result; 74 | } 75 | 76 | impl FirstRow for Vec 77 | where 78 | T: FromRow, 79 | { 80 | fn first_row(self) -> Result { 81 | match self.into_iter().nth(0) { 82 | Some(row) => Ok(row), 83 | None => Err(Error::RowNotFound), 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /static_sqlite_ffi/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_sqlite_ffi" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | 8 | [build-dependencies] 9 | bindgen = "0.71" 10 | -------------------------------------------------------------------------------- /static_sqlite_ffi/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | 4 | fn main() { 5 | // Tell cargo to look for shared libraries in the specified directory 6 | println!("cargo:rustc-link-search=/usr/lib"); 7 | 8 | // Tell cargo to tell rustc to link the sqlite3 shared library 9 | println!("cargo:rustc-link-lib=sqlite3"); 10 | 11 | // Tell cargo to invalidate the built crate whenever the wrapper changes 12 | println!("cargo:rerun-if-changed=wrapper.h"); 13 | 14 | // The bindgen::Builder is the main entry point 15 | let bindings = bindgen::Builder::default() 16 | // The input header we would like to generate bindings for 17 | .header("wrapper.h") 18 | .derive_default(false) 19 | // Tell cargo to invalidate the built crate whenever any of the 20 | // included header files changed 21 | .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) 22 | // Generate bindings 23 | .generate() 24 | .expect("Unable to generate bindings"); 25 | 26 | // Write the bindings to the $OUT_DIR/bindings.rs file. 27 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 28 | bindings 29 | .write_to_file(out_path.join("bindings.rs")) 30 | .expect("Couldn't write bindings!"); 31 | } 32 | -------------------------------------------------------------------------------- /static_sqlite_ffi/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | // Include the generated bindings 6 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 7 | 8 | // Example function using the bindings 9 | pub fn sqlite_version() -> String { 10 | unsafe { 11 | let version = sqlite3_libversion(); 12 | std::ffi::CStr::from_ptr(version) 13 | .to_string_lossy() 14 | .into_owned() 15 | } 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn test_version() { 24 | let version = sqlite_version(); 25 | println!("SQLite version: {}", version); 26 | assert!(!version.is_empty()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /static_sqlite_ffi/wrapper.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /static_sqlite_macros/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 = "log" 7 | version = "0.4.22" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 10 | 11 | [[package]] 12 | name = "proc-macro2" 13 | version = "1.0.92" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 16 | dependencies = [ 17 | "unicode-ident", 18 | ] 19 | 20 | [[package]] 21 | name = "quote" 22 | version = "1.0.37" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 25 | dependencies = [ 26 | "proc-macro2", 27 | ] 28 | 29 | [[package]] 30 | name = "sqlparser" 31 | version = "0.52.0" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "9a875d8cd437cc8a97e9aeaeea352ec9a19aea99c23e9effb17757291de80b08" 34 | dependencies = [ 35 | "log", 36 | "sqlparser_derive", 37 | ] 38 | 39 | [[package]] 40 | name = "sqlparser_derive" 41 | version = "0.2.2" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "01b2e185515564f15375f593fb966b5718bc624ba77fe49fa4616ad619690554" 44 | dependencies = [ 45 | "proc-macro2", 46 | "quote", 47 | "syn", 48 | ] 49 | 50 | [[package]] 51 | name = "static_sqlite_macros" 52 | version = "0.1.0" 53 | dependencies = [ 54 | "proc-macro2", 55 | "quote", 56 | "sqlparser", 57 | "syn", 58 | ] 59 | 60 | [[package]] 61 | name = "syn" 62 | version = "2.0.89" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" 65 | dependencies = [ 66 | "proc-macro2", 67 | "quote", 68 | "unicode-ident", 69 | ] 70 | 71 | [[package]] 72 | name = "unicode-ident" 73 | version = "1.0.14" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 76 | -------------------------------------------------------------------------------- /static_sqlite_macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "static_sqlite_macros" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | proc-macro = true 8 | 9 | [dependencies] 10 | proc-macro2 = "1" 11 | quote = "1" 12 | sqlparser = { version = "0.52", features = ["visitor"] } 13 | syn = { version = "2", features = ["full", "extra-traits", "parsing"] } 14 | static_sqlite_core = { path = "../static_sqlite_core" } 15 | -------------------------------------------------------------------------------- /static_sqlite_macros/src/errors.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq, Eq)] 2 | pub enum Error { 3 | MissingColumn(String), 4 | MissingTable(String), 5 | } 6 | 7 | pub type Result = core::result::Result; 8 | -------------------------------------------------------------------------------- /static_sqlite_macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::ops::ControlFlow; 3 | 4 | use proc_macro2::{Ident, Span, TokenStream}; 5 | use quote::{quote, ToTokens}; 6 | use sqlparser::ast::{ 7 | visit_relations, Delete, FromTable, Insert, SelectItem, TableFactor, TableWithJoins, 8 | }; 9 | use sqlparser::{ast::Statement, dialect::SQLiteDialect, parser::Parser}; 10 | use syn::{parse_macro_input, Error, LitStr, LocalInit, PatIdent, Result}; 11 | 12 | mod errors; 13 | mod names; 14 | 15 | use static_sqlite_core::{self as sqlite, Sqlite}; 16 | 17 | /// Make rust structs and functions from sql 18 | /// 19 | /// ``` 20 | /// # use static_sqlite::{sql, Result}; 21 | /// # 22 | /// sql! { 23 | /// let migrate = r#" 24 | /// create table Row (id integer primary key); 25 | /// "#; 26 | /// } 27 | /// 28 | /// #[tokio::main] 29 | /// async fn main() -> Result<()> { 30 | /// let db = static_sqlite::open("db.sqlite3")?; 31 | /// let _ = migrate(&db).await?; 32 | /// 33 | /// Ok(()) 34 | /// } 35 | /// ``` 36 | /// 37 | /// The migration sql string is required. You can call it whatever you want 38 | /// but you should have some sql in there, because that is what the rest of macro 39 | /// uses to determine the schema. This macro is tokens in tokens out, there are no 40 | /// side effects, no files written. Just tokens. Which is why you need to either 41 | /// define your sqlite schema in that migrate fn. Each sql statement in that migrate 42 | /// string is a migration. Migrations are executed top to bottom. 43 | /// Each let ident becomes a function and each create/alter table sql statement becomes 44 | /// a struct. 45 | /// 46 | /// ``` 47 | /// # use static_sqlite::sql; 48 | /// # 49 | /// sql! { 50 | /// let migrate = r#" 51 | /// create table Row (id integer primary key); 52 | /// alter table Row add column updated_at integer; 53 | /// "#; 54 | /// 55 | /// let insert_row = r#" 56 | /// insert into Row (updated_at) 57 | /// values (?) 58 | /// returning * 59 | /// "#; 60 | /// } 61 | /// 62 | /// #[tokio::main] 63 | /// async fn main() -> Result<()> { 64 | /// let db = static_sqlite::open("db.sqlite3")?; 65 | /// let _ = migrate(&db).await?; 66 | /// let row = insert_row(&db, 0).await?; 67 | /// 68 | /// Ok(()) 69 | /// } 70 | /// ``` 71 | #[proc_macro] 72 | pub fn sql(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 73 | let SqlExprs(sql_exprs) = parse_macro_input!(input as SqlExprs); 74 | match sql_macro(sql_exprs) { 75 | Ok(s) => s.to_token_stream().into(), 76 | Err(e) => e.to_compile_error().into(), 77 | } 78 | } 79 | 80 | // There are four things we want to do 81 | // 1. Parse the sql, return any parsing errors (this happens when parsing the input impl syn::parse::Parse for SqlExprs) 82 | // 2. Run the sql against an in memory sqlite db, looking for any sqlite errors 83 | // 3. Generate the structs from the migrate expr (the one with only ddl sql statements) 84 | // 4. Generate the fns from the other idents in the sql! macro 85 | fn sql_macro(exprs: Vec) -> syn::Result { 86 | let (migrate_expr, exprs) = split_exprs(&exprs)?; 87 | let db = match sqlite::open(":memory:") { 88 | Ok(db) => db, 89 | Err(err) => return Err(syn::Error::new(Span::call_site(), err)), 90 | }; 91 | if let Err(err) = db.execute_all("PRAGMA foreign_keys = ON;") { 92 | return Err(syn::Error::new(Span::call_site(), err)); 93 | }; 94 | // validate migrate expr 95 | for stmt in &migrate_expr.statements { 96 | if let Err(err) = db.execute_all(&stmt.to_string()) { 97 | return Err(syn::Error::new(migrate_expr.ident.span(), err)); 98 | } 99 | } 100 | let _ = names::validate_migrate_expr(&migrate_expr)?; 101 | let migrate_fn = migrate_fn(&migrate_expr); 102 | let schema = schema(&db); 103 | let structs = structs_tokens(migrate_expr.ident.span(), &schema); 104 | let fns = fn_tokens(&db, &schema, &exprs)?; 105 | let traits = trait_tokens(&schema, &exprs); 106 | let output = quote! { 107 | #(#structs)* 108 | #(#fns)* 109 | #(#traits)* 110 | #migrate_fn 111 | }; 112 | 113 | Ok(output) 114 | } 115 | 116 | fn trait_parts(statement: &Statement) -> Option<(String, Option<&[SelectItem]>)> { 117 | match &statement { 118 | Statement::Insert(Insert { 119 | table_name, 120 | returning, 121 | .. 122 | }) => Some(( 123 | table_name.to_string(), 124 | returning.as_ref().map(|x| x.as_slice()), 125 | )), 126 | Statement::Update { 127 | table, returning, .. 128 | } => match &table.relation { 129 | TableFactor::Table { name, .. } => { 130 | Some((name.to_string(), returning.as_ref().map(|x| x.as_slice()))) 131 | } 132 | _ => None, 133 | }, 134 | Statement::Delete(Delete { 135 | from, returning, .. 136 | }) => match from { 137 | FromTable::WithFromKeyword(table) => match &table[..] { 138 | [TableWithJoins { relation, .. }] => match relation { 139 | TableFactor::Table { name, .. } => { 140 | Some((name.to_string(), returning.as_ref().map(|x| x.as_slice()))) 141 | } 142 | _ => None, 143 | }, 144 | _ => None, 145 | }, 146 | _ => None, 147 | }, 148 | Statement::Query(query) => match query.body.as_ref() { 149 | sqlparser::ast::SetExpr::Select(ref select) => { 150 | let table = &select.from; 151 | match &table[..] { 152 | [TableWithJoins { relation, .. }] => match relation { 153 | TableFactor::Table { name, .. } => { 154 | Some((name.to_string(), Some(select.projection.as_slice()))) 155 | } 156 | _ => None, 157 | }, 158 | _ => None, 159 | } 160 | } 161 | _ => None, 162 | }, 163 | _ => None, 164 | } 165 | } 166 | 167 | fn trait_tokens(schema: &HashMap>, exprs: &[&SqlExpr]) -> Vec { 168 | exprs 169 | .iter() 170 | .flat_map(|expr| { 171 | let ident = &expr.ident; 172 | let query_ident = snake_to_pascal_case(&ident); 173 | expr.statements 174 | .iter() 175 | .filter_map(trait_parts) 176 | .map(|(table_name, returning)| match returning { 177 | Some(select_items) => { 178 | let fields: Vec<_> = select_items 179 | .iter() 180 | .flat_map(|si| match si { 181 | sqlparser::ast::SelectItem::UnnamedExpr(sql_expr) => match sql_expr 182 | { 183 | sqlparser::ast::Expr::Identifier(ident) => { 184 | vec![Ident::new(&ident.to_string(), expr.ident.span())] 185 | } 186 | _ => todo!(), 187 | }, 188 | sqlparser::ast::SelectItem::ExprWithAlias { 189 | expr: sql_expr, 190 | alias: _, 191 | } => match sql_expr { 192 | sqlparser::ast::Expr::Identifier(ident) => { 193 | vec![Ident::new(&ident.to_string(), expr.ident.span())] 194 | } 195 | _ => todo!(), 196 | }, 197 | sqlparser::ast::SelectItem::QualifiedWildcard( 198 | object_name, 199 | _wildcard_additional_options, 200 | ) => match schema.get(&object_name.to_string()) { 201 | Some(rows) => rows 202 | .iter() 203 | .map(|row| Ident::new(&row.column_name, expr.ident.span())) 204 | .collect::>(), 205 | None => todo!(), 206 | }, 207 | sqlparser::ast::SelectItem::Wildcard( 208 | _wildcard_additional_options, 209 | ) => match schema.get(&table_name) { 210 | Some(rows) => rows 211 | .iter() 212 | .map(|row| Ident::new(&row.column_name, expr.ident.span())) 213 | .collect::>(), 214 | None => todo!(), 215 | }, 216 | }) 217 | .collect(); 218 | let table_ident = Ident::new(&table_name, expr.ident.span()); 219 | quote! { 220 | impl From<#query_ident> for #table_ident { 221 | fn from(#query_ident { #(#fields,)* }: #query_ident) -> Self { 222 | Self { 223 | #(#fields,)* 224 | ..Default::default() 225 | } 226 | } 227 | } 228 | } 229 | } 230 | None => quote! {}, 231 | }) 232 | .collect::>() 233 | }) 234 | .collect::>() 235 | } 236 | 237 | fn input_column_names(db: &Sqlite, expr: &SqlExpr) -> syn::Result> { 238 | let mut output = vec![]; 239 | match db.bind_param_names(&expr.sql) { 240 | Ok(names) => output.extend(names), 241 | Err(err) => return Err(syn::Error::new(expr.ident.span(), err.to_string())), 242 | } 243 | Ok(output) 244 | } 245 | 246 | fn output_column_names(db: &Sqlite, expr: &SqlExpr) -> syn::Result> { 247 | let mut output = vec![]; 248 | match db.aliased_column_names(&expr.sql) { 249 | Ok(names) => output.extend(names), 250 | Err(err) => return Err(syn::Error::new(expr.ident.span(), err.to_string())), 251 | } 252 | Ok(output) 253 | } 254 | 255 | fn table_names(db: &Sqlite, expr: &SqlExpr) -> syn::Result> { 256 | let mut output = vec![]; 257 | for stmt in &expr.statements { 258 | match stmt { 259 | Statement::Insert(Insert { table_name, .. }) => output.push(table_name.to_string()), 260 | Statement::Update { table, .. } => output.extend(table_with_joins(&table)), 261 | Statement::Delete(Delete { tables, from, .. }) => { 262 | output.extend(tables.iter().map(|table| table.to_string())); 263 | match from { 264 | sqlparser::ast::FromTable::WithFromKeyword(vec) => { 265 | output.extend(vec.iter().flat_map(table_with_joins)); 266 | } 267 | sqlparser::ast::FromTable::WithoutKeyword(vec) => { 268 | output.extend(vec.iter().flat_map(table_with_joins)); 269 | } 270 | }; 271 | } 272 | // easier to grab the table names from the sqlite c api 273 | Statement::Query(_) => output.extend(db.table_names(&expr.sql).unwrap_or_default()), 274 | _ => todo!(), 275 | } 276 | } 277 | Ok(output) 278 | } 279 | 280 | fn table_with_joins(table: &TableWithJoins) -> Vec { 281 | let mut output = vec![]; 282 | output.extend(table_factor_tables(&table.relation)); 283 | 284 | for join in &table.joins { 285 | output.extend(table_factor_tables(&join.relation)); 286 | } 287 | 288 | output 289 | } 290 | 291 | fn table_factor_tables(table_factor: &TableFactor) -> Vec { 292 | match table_factor { 293 | TableFactor::Table { name, .. } => vec![name.to_string()], 294 | _ => todo!(), 295 | } 296 | } 297 | 298 | #[derive(Debug, Default, Clone)] 299 | struct SchemaRow { 300 | table_name: String, 301 | column_name: String, 302 | column_type: String, 303 | not_null: i64, 304 | pk: i64, 305 | } 306 | 307 | impl static_sqlite_core::FromRow for SchemaRow { 308 | fn from_row( 309 | columns: Vec<(String, static_sqlite_core::Value)>, 310 | ) -> static_sqlite_core::Result { 311 | let mut row = SchemaRow::default(); 312 | for (name, value) in columns { 313 | match name.as_str() { 314 | "table_name" => row.table_name = value.try_into()?, 315 | "column_name" => row.column_name = value.try_into()?, 316 | "column_type" => row.column_type = value.try_into()?, 317 | "not_null" => row.not_null = value.try_into()?, 318 | "pk" => row.pk = value.try_into()?, 319 | _ => {} 320 | } 321 | } 322 | 323 | Ok(row) 324 | } 325 | } 326 | 327 | fn schema(db: &Sqlite) -> HashMap> { 328 | let rows: static_sqlite_core::Result> = db.query( 329 | r#" 330 | select 331 | m.tbl_name as table_name, 332 | p.name as column_name, 333 | p."notnull" as not_null, 334 | p.pk, 335 | p.type as column_type 336 | from sqlite_master m 337 | join pragma_table_info(m.name) p 338 | where m.type = 'table' 339 | and m.tbl_name not like 'sqlite_%' 340 | order by 341 | m.tbl_name, 342 | p.cid;"#, 343 | &[], 344 | ); 345 | 346 | match rows { 347 | Ok(rows) => rows.into_iter().fold(HashMap::new(), |mut acc, row| { 348 | acc.entry(row.table_name.clone()) 349 | .or_insert_with(Vec::new) 350 | .push(row); 351 | acc 352 | }), 353 | Err(_) => todo!(), 354 | } 355 | } 356 | 357 | // Splits the SqlExpr into migrate (the first found ddl only one) and the others 358 | fn split_exprs<'a>(exprs: &'a Vec) -> Result<(&'a SqlExpr, Vec<&'a SqlExpr>)> { 359 | // we need to find the one expr that has all ddl statements 360 | // treat this as the migrate fn 361 | // this also grabs the db schema 362 | let mut iter = exprs.iter(); 363 | let migrate_expr = iter.find(|expr| is_ddl(expr)).ok_or(syn::Error::new( 364 | Span::call_site(), 365 | r#"You need a migration fn. Try this: 366 | let migrate = r\#"create table YourTable (id integer primary key);"\#; 367 | "#, 368 | ))?; 369 | let exprs = iter.filter(|ex| !is_ddl(ex)).collect(); 370 | 371 | Ok((migrate_expr, exprs)) 372 | } 373 | 374 | /// Generates the migrate fn tokens 375 | /// 376 | /// It uses a sqlite savepoint to either 377 | /// migrate everything or rollback the transaction 378 | /// if something failed 379 | fn migrate_fn(expr: &SqlExpr) -> TokenStream { 380 | let SqlExpr { ident, sql, .. } = expr; 381 | 382 | quote! { 383 | pub async fn #ident(sqlite: &static_sqlite::Sqlite) -> Result<()> { 384 | let sql = #sql.to_string(); 385 | let _ = static_sqlite::execute_all(&sqlite, "create table if not exists __migrations__ (sql text primary key not null);".into()).await?; 386 | for stmt in sql.split(";").filter(|s| !s.trim().is_empty()) { 387 | let mig: String = stmt.chars().filter(|c| !c.is_whitespace()).collect(); 388 | let changed = static_sqlite::execute(&sqlite, "insert into __migrations__ (sql) values (:sql) on conflict (sql) do nothing".into(), vec![static_sqlite::Value::Text(mig)]).await?; 389 | if changed != 0 { 390 | let _k = static_sqlite::execute(&sqlite, stmt.to_string(), vec![]).await?; 391 | } 392 | } 393 | return Ok(()); 394 | } 395 | } 396 | } 397 | 398 | fn fn_tokens(db: &Sqlite, schema: &Schema, exprs: &[&SqlExpr]) -> Result> { 399 | let mut output = vec![]; 400 | for expr in exprs { 401 | if let None = expr.statements.last() { 402 | return Err(syn::Error::new( 403 | expr.ident.span(), 404 | "At least one sql statement is required", 405 | )); 406 | } 407 | 408 | let inputs = input_column_names(db, expr)?; 409 | let inputs: Vec<_> = inputs 410 | .iter() 411 | .map(|input| input.replacen(":", "", 1)) 412 | .collect(); 413 | let mut table_names = table_names(db, expr)?; 414 | // get joined table names that might not exist in select clause 415 | table_names.extend(join_table_names(expr)); 416 | let mut schema_rows = vec![]; 417 | for table_name in &table_names { 418 | match schema.get(table_name) { 419 | Some(rows) => schema_rows.extend(rows), 420 | None => {} 421 | }; 422 | } 423 | let input_schema_rows: Vec<&&SchemaRow> = inputs 424 | .iter() 425 | .filter_map(|col_name| schema_rows.iter().find(|row| &row.column_name == col_name)) 426 | .collect(); 427 | let fn_args = input_schema_rows 428 | .iter() 429 | .map(|field| { 430 | let field_type = match field.column_type.as_str() { 431 | "BLOB" => quote! { Vec }, 432 | "INTEGER" => quote! { i64 }, 433 | "REAL" | "DOUBLE" => quote! { f64 }, 434 | "TEXT" => quote! { impl ToString }, 435 | _ => unimplemented!("Sqlite fn arg not supported"), 436 | }; 437 | let field_name = Ident::new(&field.column_name, expr.ident.span()); 438 | let not_null = field.not_null; 439 | let pk = field.pk; 440 | match (pk, not_null) { 441 | (0, 0) => quote! { #field_name: Option<#field_type> }, 442 | _ => quote! { #field_name: #field_type }, 443 | } 444 | }) 445 | .collect::>(); 446 | let params = input_schema_rows 447 | .iter() 448 | .map(|field| { 449 | let not_null = field.not_null; 450 | let name = Ident::new(&field.column_name, expr.ident.span()); 451 | match field.column_type.as_str() { 452 | "BLOB" => { 453 | quote! { #name.into() } 454 | } 455 | "INTEGER" => quote! { #name.into() }, 456 | "REAL" | "DOUBLE" => quote! { #name.into() }, 457 | "TEXT" => match not_null { 458 | 1 => quote! { 459 | #name.to_string().into() 460 | }, 461 | 0 => quote! { 462 | match #name { 463 | Some(val) => val.to_string().into(), 464 | None => static_sqlite::Value::Null 465 | } 466 | }, 467 | _ => unreachable!(), 468 | }, 469 | _ => unimplemented!("Sqlite param not supported"), 470 | } 471 | }) 472 | .collect::>(); 473 | let ident = &expr.ident; 474 | let outputs = output_column_names(db, expr)?; 475 | let pascal_case = snake_to_pascal_case(&ident); 476 | let cols: Vec = outputs 477 | .iter() 478 | .filter_map(|col_name| { 479 | schema_rows 480 | .iter() 481 | .find(|row| &row.column_name == col_name) 482 | .cloned() 483 | .cloned() 484 | }) 485 | .collect(); 486 | let struct_tokens = struct_tokens(expr.ident.span(), &pascal_case, &cols); 487 | let sql = &expr.sql; 488 | output.push(quote! { 489 | #struct_tokens 490 | 491 | #[doc = #sql] 492 | pub async fn #ident(db: &static_sqlite::Sqlite, #(#fn_args),*) -> Result> { 493 | let rows: Vec<#pascal_case> = static_sqlite::query(db, #sql, vec![#(#params,)*]).await?; 494 | Ok(rows) 495 | } 496 | }) 497 | } 498 | Ok(output) 499 | } 500 | 501 | fn join_table_names(expr: &&SqlExpr) -> Vec { 502 | let mut output = vec![]; 503 | visit_relations(&expr.statements, |rel| { 504 | output.push(rel.to_string()); 505 | ControlFlow::<()>::Continue(()) 506 | }); 507 | output 508 | } 509 | 510 | fn snake_to_pascal_case(input: &syn::Ident) -> syn::Ident { 511 | let s = input.to_string(); 512 | let mut result = String::with_capacity(s.len()); 513 | let mut capitalize_next = true; 514 | 515 | for ch in s.chars() { 516 | if ch == '_' { 517 | capitalize_next = true; 518 | } else if capitalize_next { 519 | result.extend(ch.to_uppercase()); 520 | capitalize_next = false; 521 | } else { 522 | result.extend(ch.to_lowercase()); 523 | } 524 | } 525 | 526 | syn::Ident::new(&result, input.span()) 527 | } 528 | 529 | type Schema = HashMap>; 530 | 531 | fn structs_tokens(span: Span, schema: &Schema) -> Vec { 532 | schema 533 | .iter() 534 | .map(|(table, cols)| { 535 | let ident = proc_macro2::Ident::new(&table, span); 536 | struct_tokens(span, &ident, cols) 537 | }) 538 | .collect() 539 | } 540 | 541 | fn struct_tokens(span: Span, ident: &Ident, cols: &Vec) -> TokenStream { 542 | let struct_fields = cols.iter().map(|row| { 543 | let field_type = field_type(row); 544 | let name = Ident::new(&row.column_name, span); 545 | let optional = match (row.not_null, row.pk) { 546 | (0, 0) => true, 547 | (0, 1) | (1, 0) | (1, 1) => false, 548 | _ => unreachable!(), 549 | }; 550 | 551 | match optional { 552 | true => quote! { pub #name: Option<#field_type> }, 553 | false => quote! { pub #name: #field_type }, 554 | } 555 | }); 556 | let match_stmt = cols.iter().map(|field| { 557 | let name = Ident::new(&field.column_name, span); 558 | let lit_str = LitStr::new(&field.column_name, span); 559 | 560 | quote! { 561 | #lit_str => row.#name = value.try_into()? 562 | } 563 | }); 564 | let tokens = quote! { 565 | #[derive(Default, Debug, Clone, PartialEq)] 566 | pub struct #ident { #(#struct_fields),* } 567 | 568 | impl static_sqlite::FromRow for #ident { 569 | fn from_row(columns: Vec<(String, static_sqlite::Value)>) -> static_sqlite::Result { 570 | let mut row = #ident::default(); 571 | for (column, value) in columns { 572 | match column.as_str() { 573 | #(#match_stmt,)* 574 | _ => {} 575 | } 576 | } 577 | 578 | Ok(row) 579 | } 580 | } 581 | }; 582 | 583 | tokens 584 | } 585 | 586 | fn field_type(row: &SchemaRow) -> TokenStream { 587 | match row.column_type.as_str() { 588 | "BLOB" => quote! { Vec }, 589 | "INTEGER" => quote! { i64 }, 590 | "REAL" | "DOUBLE" => quote! { f64 }, 591 | "TEXT" => quote! { String }, 592 | _ => todo!("field_type"), 593 | } 594 | } 595 | 596 | #[derive(Clone, Debug)] 597 | struct SqlExpr { 598 | ident: Ident, 599 | sql: String, 600 | statements: Vec, 601 | } 602 | 603 | #[derive(Debug)] 604 | struct SqlExprs(pub Vec); 605 | 606 | impl syn::parse::Parse for SqlExprs { 607 | fn parse(input: syn::parse::ParseStream) -> syn::Result { 608 | let mut sql_exprs: Vec = Vec::new(); 609 | while !input.is_empty() { 610 | let stmt: syn::Stmt = input.parse()?; 611 | let sql_expr = sql_expr(stmt)?; 612 | sql_exprs.push(sql_expr); 613 | } 614 | Ok(SqlExprs(sql_exprs)) 615 | } 616 | } 617 | 618 | fn sql_expr(stmt: syn::Stmt) -> Result { 619 | match stmt { 620 | syn::Stmt::Local(syn::Local { pat, init, .. }) => { 621 | let ident = match pat { 622 | syn::Pat::Ident(PatIdent { ident, .. }) => ident, 623 | _ => unimplemented!("Only idents are supported for sql"), 624 | }; 625 | let sql = match init { 626 | Some(LocalInit { expr, .. }) => match *expr { 627 | syn::Expr::Lit(syn::ExprLit { 628 | lit: syn::Lit::Str(lit_str), 629 | .. 630 | }) => lit_str.value(), 631 | _ => return Err(Error::new_spanned(ident, "sql is missing")), 632 | }, 633 | None => return Err(Error::new_spanned(ident, "sql is missing")), 634 | }; 635 | let statements = match Parser::parse_sql(&SQLiteDialect {}, &sql) { 636 | Ok(ast) => ast, 637 | Err(err) => { 638 | // TODO: better error handling 639 | return Err(Error::new_spanned(ident, err.to_string())); 640 | } 641 | }; 642 | Ok(SqlExpr { 643 | ident, 644 | sql, 645 | statements, 646 | }) 647 | } 648 | syn::Stmt::Item(_) => todo!("todo item"), 649 | syn::Stmt::Expr(_, _) => todo!("todo expr"), 650 | syn::Stmt::Macro(_) => todo!("todo macro"), 651 | } 652 | } 653 | 654 | fn is_ddl(expr: &SqlExpr) -> bool { 655 | expr.statements.iter().all(|stmt| match stmt { 656 | Statement::CreateView { .. } 657 | | Statement::CreateTable { .. } 658 | | Statement::CreateVirtualTable { .. } 659 | | Statement::CreateIndex { .. } 660 | | Statement::CreateRole { .. } 661 | | Statement::AlterTable { .. } 662 | | Statement::AlterIndex { .. } 663 | | Statement::AlterView { .. } 664 | | Statement::AlterRole { .. } 665 | | Statement::Drop { .. } 666 | | Statement::DropFunction { .. } 667 | | Statement::CreateExtension { .. } 668 | | Statement::SetNamesDefault {} 669 | | Statement::ShowFunctions { .. } 670 | | Statement::ShowVariable { .. } 671 | | Statement::ShowVariables { .. } 672 | | Statement::ShowCreate { .. } 673 | | Statement::ShowColumns { .. } 674 | | Statement::ShowTables { .. } 675 | | Statement::ShowCollation { .. } 676 | | Statement::Use { .. } 677 | | Statement::StartTransaction { .. } 678 | | Statement::SetTransaction { .. } 679 | | Statement::Comment { .. } 680 | | Statement::Commit { .. } 681 | | Statement::Rollback { .. } 682 | | Statement::CreateSchema { .. } 683 | | Statement::CreateDatabase { .. } 684 | | Statement::CreateFunction { .. } 685 | | Statement::CreateProcedure { .. } 686 | | Statement::CreateMacro { .. } 687 | | Statement::CreateStage { .. } 688 | | Statement::Prepare { .. } 689 | | Statement::ExplainTable { .. } 690 | | Statement::Explain { .. } 691 | | Statement::Savepoint { .. } 692 | | Statement::ReleaseSavepoint { .. } 693 | | Statement::CreateSequence { .. } 694 | | Statement::CreateType { .. } 695 | | Statement::Pragma { .. } => true, 696 | _ => false, 697 | }) 698 | } 699 | -------------------------------------------------------------------------------- /static_sqlite_macros/src/names.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::{Error, Result}, 3 | SqlExpr, 4 | }; 5 | use sqlparser::ast::{ 6 | visit_expressions, visit_relations, visit_statements, BinaryOperator, ColumnOption, 7 | CreateTable, Expr, ObjectName, Statement, TableFactor, Visit, Visitor, 8 | }; 9 | use std::{collections::HashMap, ops::ControlFlow}; 10 | 11 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 12 | pub enum Name { 13 | Table(Vec), 14 | Col(Vec), 15 | } 16 | 17 | #[allow(unused)] 18 | pub fn names(statements: &Vec) -> Vec { 19 | let mut visited = vec![]; 20 | visit_relations(statements, |ObjectName(parts)| { 21 | visited.push(Name::Table( 22 | parts.iter().map(|ident| ident.value.clone()).collect(), 23 | )); 24 | ControlFlow::<()>::Continue(()) 25 | }); 26 | visit_expressions(statements, |expr| { 27 | let names = name_from_expr(expr); 28 | visited.extend(names); 29 | ControlFlow::<()>::Continue(()) 30 | }); 31 | visit_statements(statements, |statement| { 32 | match statement { 33 | Statement::CreateTable(CreateTable { name, columns, .. }) => { 34 | visited.extend( 35 | columns 36 | .iter() 37 | .map(|def| { 38 | Name::Col(vec![ 39 | name.0.last().unwrap().value.clone(), 40 | def.name.value.clone(), 41 | ]) 42 | }) 43 | .collect::>(), 44 | ); 45 | visited.extend( 46 | columns 47 | .iter() 48 | .flat_map(|def| { 49 | def.options.iter().flat_map(|opt| match &opt.option { 50 | ColumnOption::ForeignKey { 51 | foreign_table, 52 | referred_columns, 53 | .. 54 | } => { 55 | let mut v = vec![]; 56 | let table = foreign_table 57 | .0 58 | .iter() 59 | .map(|ident| ident.value.clone()) 60 | .collect(); 61 | v.push(Name::Table(table)); 62 | 63 | for col in referred_columns { 64 | v.push(Name::Col(vec![col.value.clone()])) 65 | } 66 | v 67 | } 68 | _ => Vec::with_capacity(0), 69 | }) 70 | }) 71 | .collect::>(), 72 | ); 73 | } 74 | _statement => {} 75 | } 76 | ControlFlow::<()>::Continue(()) 77 | }); 78 | visited 79 | } 80 | 81 | #[allow(unused)] 82 | #[derive(Default)] 83 | struct AliasVisitor(HashMap); 84 | 85 | impl Visitor for AliasVisitor { 86 | type Break = (); 87 | 88 | fn post_visit_table_factor( 89 | &mut self, 90 | table_factor: &sqlparser::ast::TableFactor, 91 | ) -> ControlFlow { 92 | match table_factor { 93 | TableFactor::Table { name, alias, .. } => match alias { 94 | Some(table_alias) => { 95 | self.0.insert( 96 | Name::Table(name.0.iter().map(|ident| ident.value.clone()).collect()), 97 | Name::Table(vec![table_alias.name.value.clone()]), 98 | ); 99 | self.0.insert( 100 | Name::Table(vec![table_alias.name.value.clone()]), 101 | Name::Table(name.0.iter().map(|ident| ident.value.clone()).collect()), 102 | ); 103 | } 104 | 105 | None => {} 106 | }, 107 | _ => {} 108 | } 109 | ControlFlow::<()>::Continue(()) 110 | } 111 | } 112 | 113 | #[allow(unused)] 114 | pub fn aliases(statements: &Vec) -> HashMap { 115 | let mut visitor = AliasVisitor::default(); 116 | statements.iter().for_each(|statement| { 117 | statement.visit(&mut visitor); 118 | }); 119 | visitor.0 120 | } 121 | 122 | #[allow(unused)] 123 | fn name_from_expr(expr: &Expr) -> Vec { 124 | match expr { 125 | Expr::Identifier(ident) => vec![Name::Col(vec![ident.value.clone()])], 126 | Expr::CompoundIdentifier(vec) => { 127 | vec![Name::Col( 128 | vec.iter().map(|ident| ident.value.clone()).collect(), 129 | )] 130 | } 131 | Expr::Wildcard => todo!(), 132 | Expr::QualifiedWildcard(ObjectName(parts)) => vec![Name::Table( 133 | parts.iter().map(|ident| ident.value.clone()).collect(), 134 | )], 135 | Expr::BinaryOp { left, op, right } => match op { 136 | BinaryOperator::Gt => todo!(), 137 | BinaryOperator::Lt => todo!(), 138 | BinaryOperator::GtEq => todo!(), 139 | BinaryOperator::LtEq => todo!(), 140 | BinaryOperator::Spaceship => todo!(), 141 | BinaryOperator::Eq => { 142 | let mut cols = name_from_expr(left.as_ref()); 143 | cols.extend(name_from_expr(right.as_ref())); 144 | cols 145 | } 146 | BinaryOperator::NotEq => todo!(), 147 | BinaryOperator::And => todo!(), 148 | BinaryOperator::Or => todo!(), 149 | BinaryOperator::Xor => todo!(), 150 | _ => vec![], 151 | }, 152 | expr => { 153 | println!("{expr}"); 154 | vec![] 155 | } 156 | } 157 | } 158 | 159 | fn table(s1: impl core::fmt::Display) -> Name { 160 | Name::Table(vec![s1.to_string()]) 161 | } 162 | 163 | fn col(s1: impl core::fmt::Display, s2: impl core::fmt::Display) -> Name { 164 | Name::Col(vec![s1.to_string(), s2.to_string()]) 165 | } 166 | 167 | fn table_name(object_name: &ObjectName) -> Name { 168 | Name::Table(object_name.0.iter().map(|x| x.value.clone()).collect()) 169 | } 170 | 171 | fn col_name(ident: &sqlparser::ast::Ident) -> Name { 172 | Name::Col(vec![ident.value.clone()]) 173 | } 174 | 175 | pub fn validate_migrate_expr(sql_expr: &SqlExpr) -> syn::Result<()> { 176 | // first run get table name and column names from create table 177 | let tables: Vec = sql_expr 178 | .statements 179 | .iter() 180 | .flat_map(|stmt| match stmt { 181 | Statement::CreateTable(CreateTable { name, columns, .. }) => { 182 | let mut v = vec![]; 183 | v.push(table_name(name)); 184 | v.extend(columns.iter().map(|col| col_name(&col.name))); 185 | v 186 | } 187 | _ => vec![], 188 | }) 189 | .collect(); 190 | // second run look at foreign keys 191 | let refs: Vec = sql_expr 192 | .statements 193 | .iter() 194 | .flat_map(|stmt| match stmt { 195 | Statement::CreateTable(CreateTable { columns, .. }) => columns 196 | .iter() 197 | .flat_map(|col| { 198 | col.options 199 | .iter() 200 | .flat_map(|opt| match &opt.option { 201 | ColumnOption::ForeignKey { 202 | foreign_table, 203 | referred_columns, 204 | .. 205 | } => { 206 | let mut v = vec![]; 207 | v.push(table_name(&foreign_table)); 208 | v.extend( 209 | referred_columns 210 | .iter() 211 | .map(|col| Name::Col(vec![col.value.clone()])), 212 | ); 213 | v 214 | } 215 | _ => vec![], 216 | }) 217 | .collect::>() 218 | }) 219 | .collect::>(), 220 | _ => { 221 | vec![] 222 | } 223 | }) 224 | .collect(); 225 | 226 | match validate_query(&tables, &HashMap::new(), &refs) { 227 | Ok(_) => Ok(()), 228 | Err(err) => Err(syn::Error::new(sql_expr.ident.span(), format!("{:?}", err))), 229 | } 230 | } 231 | 232 | pub fn validate_query( 233 | db_schema: &Vec, 234 | aliases: &HashMap, 235 | query_schema: &Vec, 236 | ) -> Result<()> { 237 | query_schema.iter().try_for_each(|name| match name { 238 | Name::Table(vec) => match db_schema.contains(name) { 239 | true => Ok(()), 240 | false => Err(Error::MissingTable(vec.join("."))), 241 | }, 242 | Name::Col(vec) => match db_schema.contains(name) { 243 | true => Ok(()), 244 | false => { 245 | let (table_name, column_name) = match vec.as_slice() { 246 | [_schema, table_name, column_name] => (table_name, column_name), 247 | [table_name, column_name] => (table_name, column_name), 248 | [column_name] => (&String::new(), column_name), 249 | val => todo!("{:?}", val), 250 | }; 251 | 252 | match aliases.get(&table(table_name.clone())) { 253 | Some(Name::Table(parts)) => { 254 | let aliased_table_name = match parts.as_slice() { 255 | [_schema, table] => table, 256 | [table] => table, 257 | _ => todo!(), 258 | }; 259 | let column = col(aliased_table_name, column_name); 260 | match db_schema.contains(&column) { 261 | true => Ok(()), 262 | false => Err(Error::MissingColumn(format!( 263 | "{aliased_table_name}.{column_name}" 264 | ))), 265 | } 266 | } 267 | Some(Name::Col(_)) => todo!(), 268 | None => Err(Error::MissingColumn(vec.join("."))), 269 | } 270 | } 271 | }, 272 | })?; 273 | Ok(()) 274 | } 275 | 276 | #[cfg(test)] 277 | mod tests { 278 | use super::*; 279 | use sqlparser::{ast::Statement, dialect::SQLiteDialect, parser::Parser}; 280 | 281 | fn parse_sql(sql: &'static str) -> Vec { 282 | Parser::new(&SQLiteDialect {}) 283 | .try_with_sql(sql) 284 | .unwrap() 285 | .parse_statements() 286 | .unwrap() 287 | } 288 | 289 | #[test] 290 | fn names_works() { 291 | let statements = 292 | parse_sql("select u.id, t.id from User u join Todo as t on t.user_id == u.id"); 293 | let names = names(&statements); 294 | let left: Vec = vec![ 295 | table("User"), 296 | table("Todo"), 297 | col("u", "id"), 298 | col("t", "id"), 299 | col("t", "user_id"), 300 | col("u", "id"), 301 | col("t", "user_id"), 302 | col("u", "id"), 303 | ]; 304 | 305 | assert_eq!(left, names); 306 | } 307 | 308 | #[test] 309 | fn names_from_schema_works() { 310 | let statements = 311 | parse_sql("create table User (id integer primary key, email text unique not null)"); 312 | let names = names(&statements); 313 | let left: Vec = vec![table("User"), col("User", "id"), col("User", "email")]; 314 | 315 | assert_eq!(left, names); 316 | } 317 | 318 | #[test] 319 | fn aliases_works() { 320 | let statements = 321 | parse_sql("select u.id, t.id from User u join Todo as t on t.user_id == u.id"); 322 | let names = aliases(&statements); 323 | let left: HashMap = HashMap::from([ 324 | (table("User"), table("u")), 325 | (table("u"), table("User")), 326 | (table("Todo"), table("t")), 327 | (table("t"), table("Todo")), 328 | ]); 329 | 330 | assert_eq!(left, names); 331 | } 332 | 333 | #[test] 334 | fn validate_works() { 335 | let statements = 336 | parse_sql("select u.id, t.id from User u join Todo as t on t.user_id == u.id"); 337 | let create_statements = parse_sql( 338 | r#"create table User (id integer primary key, email text unique not null); 339 | create table Todo (id integer primary key, user_id integer not null references User(id))"#, 340 | ); 341 | let db_schema = names(&create_statements); 342 | let query_schema = names(&statements); 343 | let aliases = aliases(&statements); 344 | let result = validate_query(&db_schema, &aliases, &query_schema); 345 | 346 | assert_eq!(Ok::<(), Error>(()), result); 347 | 348 | let db_schema: Vec = vec![table("User"), col("User", "id")]; 349 | let query_schema: Vec = vec![table("User"), col("User", "id2")]; 350 | let result = validate_query(&db_schema, &HashMap::default(), &query_schema); 351 | 352 | assert_eq!(Err(Error::MissingColumn("User.id2".into())), result); 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /static_sqlite_macros/src/schema.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Span; 2 | use sqlparser::ast::{ 3 | AlterTableOperation, ColumnDef, ColumnOption, Expr, FunctionArg, FunctionArgExpr, Ident, 4 | ObjectName, ObjectType, Query, Select, SelectItem, SetExpr, Statement, TableFactor, 5 | TableWithJoins, Value, 6 | }; 7 | use std::collections::HashMap; 8 | use syn::{Error, Result}; 9 | 10 | use crate::SqlExpr; 11 | 12 | #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] 13 | pub struct Table<'a>(pub &'a ObjectName); 14 | 15 | #[derive(Debug, Eq, Hash, PartialEq, Clone, Copy)] 16 | pub struct Column<'a> { 17 | pub name: &'a sqlparser::ast::Ident, 18 | pub def: Option<&'a ColumnDef>, 19 | pub placeholder: Option<&'a str>, 20 | } 21 | 22 | #[derive(Debug)] 23 | pub struct Schema<'a>(pub HashMap, Vec>>); 24 | 25 | pub fn db_schema<'a>(migrate_expr: &'a SqlExpr) -> Result> { 26 | let mut set: HashMap, Vec>> = HashMap::new(); 27 | 28 | let SqlExpr { 29 | ident, statements, .. 30 | } = migrate_expr; 31 | let span = ident.span(); 32 | let _result = statements.iter().try_for_each(|stmt| match stmt { 33 | Statement::CreateTable { name, columns, .. } => { 34 | let columns = columns 35 | .iter() 36 | .map(|col| Column { 37 | name: &col.name, 38 | def: Some(col), 39 | placeholder: None, 40 | }) 41 | .collect(); 42 | set.insert(Table(name), columns); 43 | Ok(()) 44 | } 45 | Statement::AlterTable { 46 | name, operations, .. 47 | } => { 48 | for op in operations { 49 | match op { 50 | AlterTableOperation::AddColumn { column_def, .. } => { 51 | match set.get_mut(&Table(name)) { 52 | Some(columns) => columns.push(Column { 53 | name: &column_def.name, 54 | def: Some(column_def), 55 | placeholder: None, 56 | }), 57 | None => todo!(), 58 | }; 59 | } 60 | AlterTableOperation::DropColumn { column_name, .. } => { 61 | match set.get_mut(&Table(name)) { 62 | Some(columns) => { 63 | columns.retain(|col| col.name != column_name); 64 | } 65 | None => {} 66 | } 67 | } 68 | AlterTableOperation::RenameColumn { 69 | old_column_name, 70 | new_column_name, 71 | } => match set.get_mut(&Table(name)) { 72 | Some(columns) => { 73 | match columns.iter().position(|col| col.name == old_column_name) { 74 | Some(ix) => { 75 | let mut col = columns.remove(ix); 76 | col.name = new_column_name; 77 | columns.push(col); 78 | } 79 | None => {} 80 | }; 81 | } 82 | None => {} 83 | }, 84 | AlterTableOperation::RenameTable { table_name } => { 85 | match set.remove(&Table(name)) { 86 | Some(columns) => { 87 | set.insert(Table(table_name), columns); 88 | } 89 | None => {} 90 | } 91 | } 92 | _ => {} 93 | } 94 | } 95 | Ok(()) 96 | } 97 | Statement::Drop { 98 | object_type, names, .. 99 | } => { 100 | match object_type { 101 | ObjectType::Table => match names.first() { 102 | Some(name) => { 103 | set.remove(&Table(name)); 104 | } 105 | None => { 106 | return Err(Error::new( 107 | span, 108 | format!("drop statement {} requires a name", stmt.to_string()), 109 | )) 110 | } 111 | }, 112 | _ => todo!(), 113 | } 114 | Ok(()) 115 | } 116 | _ => Ok(()), 117 | })?; 118 | 119 | Ok(Schema(set)) 120 | } 121 | 122 | pub fn query_schema<'a>(span: Span, statements: &'a Vec) -> Result> { 123 | let mut set: HashMap, Vec>> = HashMap::new(); 124 | 125 | statements.iter().try_for_each(|stmt| match stmt { 126 | Statement::Query(query) => { 127 | set_query_columns(query, span, &mut set)?; 128 | Ok(()) 129 | } 130 | Statement::Insert { 131 | table_name, 132 | columns, 133 | returning, 134 | .. 135 | } => { 136 | let table = Table(table_name); 137 | let mut columns: Vec> = columns 138 | .iter() 139 | .map(|name| Column { 140 | name, 141 | def: None, 142 | placeholder: Some("?"), 143 | }) 144 | .collect(); 145 | let returning_columns = returning_columns(table, returning); 146 | columns.extend(returning_columns); 147 | set.insert(table, columns); 148 | Ok(()) 149 | } 150 | Statement::Update { 151 | table, 152 | assignments, 153 | from: _from, 154 | selection, 155 | returning, 156 | } => { 157 | let table = match &table.relation { 158 | TableFactor::Table { name, .. } => Table(name), 159 | _ => todo!(), 160 | }; 161 | let mut columns: Vec<_> = assignments 162 | .iter() 163 | .filter_map(|assign| { 164 | let name = match assign.id.as_slice() { 165 | [_schema, _table, column] => Some(column), 166 | [_table, column] => Some(column), 167 | [column] => Some(column), 168 | _ => None, 169 | }; 170 | match name { 171 | Some(name) => match &assign.value { 172 | Expr::Value(Value::Placeholder(val)) => Some(Column { 173 | name, 174 | def: None, 175 | placeholder: Some(val.as_str()), 176 | }), 177 | _ => Some(Column { 178 | name, 179 | def: None, 180 | placeholder: None, 181 | }), 182 | }, 183 | None => None, 184 | } 185 | }) 186 | .collect(); 187 | let selection_columns = selection_columns(selection); 188 | columns.extend(selection_columns); 189 | let returning_columns = returning_columns(table, returning); 190 | columns.extend(returning_columns); 191 | set.insert(table, columns); 192 | Ok(()) 193 | } 194 | Statement::Delete { 195 | from, 196 | selection, 197 | returning, 198 | .. 199 | } => { 200 | let table = match from.first() { 201 | None => { 202 | return Err(syn::Error::new( 203 | span, 204 | "Delete statement requires at least one table", 205 | )) 206 | } 207 | Some(table) => match &table.relation { 208 | TableFactor::Table { name, .. } => Table(name), 209 | _ => todo!(), 210 | }, 211 | }; 212 | let mut columns = selection_columns(selection); 213 | let ret_columns = returning_columns(table, returning); 214 | columns.extend(ret_columns); 215 | set.insert(table, columns); 216 | Ok(()) 217 | } 218 | Statement::CreateTable { name, columns, .. } => { 219 | for column in columns { 220 | let ColumnDef { options, .. } = column; 221 | options.iter().for_each(|opt| match &opt.option { 222 | ColumnOption::ForeignKey { 223 | foreign_table, 224 | referred_columns, 225 | .. 226 | } => { 227 | set.insert( 228 | Table(foreign_table), 229 | referred_columns 230 | .iter() 231 | .map(|name| Column { 232 | name, 233 | def: None, 234 | placeholder: None, 235 | }) 236 | .collect(), 237 | ); 238 | } 239 | _ => {} 240 | }); 241 | } 242 | set.insert( 243 | Table(name), 244 | columns 245 | .iter() 246 | .map(|col| Column { 247 | name: &col.name, 248 | def: Some(&col), 249 | placeholder: None, 250 | }) 251 | .collect(), 252 | ); 253 | Ok(()) 254 | } 255 | Statement::AlterTable { .. } => Ok(()), 256 | _ => todo!("fn query_schema Statement match statement"), 257 | })?; 258 | 259 | Ok(Schema(set)) 260 | } 261 | 262 | fn set_query_columns<'a>( 263 | query: &'a Query, 264 | span: Span, 265 | set: &mut HashMap, Vec>>, 266 | ) -> Result<()> { 267 | let Query { 268 | with: _with, 269 | body, 270 | order_by, 271 | .. 272 | } = query; 273 | match body.as_ref() { 274 | SetExpr::Select(select) => { 275 | let tables = from_tables(&select.from); 276 | let table = match tables.first() { 277 | Some(table) => *table, 278 | None => return Err(Error::new(span, "Only one table in from supported for now")), 279 | }; 280 | let mut columns = selection_columns(&select.selection); 281 | let projection_columns = select_items_columns(table, select.projection.as_slice()); 282 | columns.extend(projection_columns); 283 | let order_by_columns: Vec> = order_by 284 | .iter() 285 | .flat_map(|ob| expr_columns(&ob.expr)) 286 | .collect(); 287 | columns.extend(order_by_columns); 288 | set.insert(table, columns.into_iter().collect()); 289 | } 290 | SetExpr::Query(query) => set_query_columns(query, span, set)?, 291 | _ => todo!("fn set_query_columns"), 292 | } 293 | Ok(()) 294 | } 295 | 296 | fn from_tables(from: &Vec) -> Vec> { 297 | from.iter() 298 | .flat_map(|table| { 299 | let mut tables = match &table.relation { 300 | TableFactor::Table { name, .. } => vec![Table(name)], 301 | _ => todo!("fn from_tables"), 302 | }; 303 | let join_tables: Vec<_> = table 304 | .joins 305 | .iter() 306 | .map(|join| relation_table(&join.relation)) 307 | .collect(); 308 | tables.extend(join_tables); 309 | tables 310 | }) 311 | .collect() 312 | } 313 | 314 | fn relation_table(relation: &TableFactor) -> Table<'_> { 315 | match relation { 316 | TableFactor::Table { name, .. } => Table(name), 317 | _ => todo!("fn relation_table: other TableFactors"), 318 | } 319 | } 320 | 321 | fn returning_columns<'a>( 322 | table: Table<'a>, 323 | returning: &'a Option>, 324 | ) -> Vec> { 325 | match returning { 326 | Some(select_items) => select_items_columns(table, select_items), 327 | None => vec![], 328 | } 329 | } 330 | 331 | fn select_items_columns<'a>(_table: Table<'a>, select_items: &'a [SelectItem]) -> Vec> { 332 | select_items 333 | .iter() 334 | .filter_map(|si| match si { 335 | SelectItem::UnnamedExpr(expr) => match expr { 336 | Expr::Identifier(name) => Some(Column { 337 | name, 338 | def: None, 339 | placeholder: None, 340 | }), 341 | Expr::CompoundIdentifier(name) => compound_ident_column(name), 342 | _ => todo!("fn select_items_columns selectitem match"), 343 | }, 344 | SelectItem::ExprWithAlias { expr, .. } => match expr { 345 | Expr::Identifier(name) => Some(Column { 346 | name, 347 | def: None, 348 | placeholder: None, 349 | }), 350 | Expr::CompoundIdentifier(name) => compound_ident_column(name), 351 | expr => todo!("fn select_items_columns ExprWithAlias {expr}"), 352 | }, 353 | _ => None, 354 | }) 355 | .collect() 356 | } 357 | 358 | fn compound_ident_column<'a>(name: &'a Vec) -> Option> { 359 | let name = match name.as_slice() { 360 | [_schema, _table, name] => Some(name), 361 | [_table, name] => Some(name), 362 | [name] => Some(name), 363 | _ => None, 364 | }; 365 | 366 | match name { 367 | Some(name) => Some(Column { 368 | name, 369 | def: None, 370 | placeholder: None, 371 | }), 372 | None => None, 373 | } 374 | } 375 | 376 | fn selection_columns<'a>(selection: &'a Option) -> Vec> { 377 | match selection { 378 | Some(expr) => expr_columns(expr), 379 | None => vec![], 380 | } 381 | } 382 | 383 | fn expr_columns<'a>(expr: &'a Expr) -> Vec> { 384 | match expr { 385 | Expr::BinaryOp { left, op: _, right } => match (left.as_ref(), right.as_ref()) { 386 | (Expr::Identifier(name), Expr::Value(Value::Placeholder(val))) if val == "?" => { 387 | vec![Column { 388 | name, 389 | def: None, 390 | placeholder: Some(val.as_str()), 391 | }] 392 | } 393 | (Expr::CompoundIdentifier(parts), Expr::Value(Value::Placeholder(val))) 394 | if val == "?" => 395 | { 396 | match parts.as_slice() { 397 | [_schema_name, _table_name, name] => { 398 | vec![Column { 399 | name, 400 | def: None, 401 | placeholder: Some(val.as_str()), 402 | }] 403 | } 404 | [_table_name, name] => { 405 | vec![Column { 406 | name, 407 | def: None, 408 | placeholder: Some(val.as_str()), 409 | }] 410 | } 411 | _ => unreachable!("one part compound identifier?!"), 412 | } 413 | } 414 | (left, right) => { 415 | let mut columns = expr_columns(left); 416 | columns.extend(expr_columns(right)); 417 | columns 418 | } 419 | }, 420 | Expr::CompoundIdentifier(parts) => { 421 | let name = match parts.as_slice() { 422 | [_schema, _table, name] => Some(name), 423 | [_table, name] => Some(name), 424 | [name] => Some(name), 425 | _ => None, 426 | }; 427 | 428 | match name { 429 | Some(name) => vec![Column { 430 | name, 431 | def: None, 432 | placeholder: None, 433 | }], 434 | None => vec![], 435 | } 436 | } 437 | Expr::Identifier(name) => vec![Column { 438 | name, 439 | def: None, 440 | placeholder: None, 441 | }], 442 | Expr::Nested(expr) => expr_columns(expr), 443 | Expr::Function(func) => func 444 | .args 445 | .iter() 446 | .flat_map(|arg| match arg { 447 | FunctionArg::Named { name: _name, arg } => match arg { 448 | FunctionArgExpr::Expr(expr) => expr_columns(expr), 449 | FunctionArgExpr::QualifiedWildcard(_object_name) => todo!(), 450 | FunctionArgExpr::Wildcard => todo!(), 451 | }, 452 | FunctionArg::Unnamed(function_arg_expr) => match function_arg_expr { 453 | FunctionArgExpr::Expr(expr) => expr_columns(expr), 454 | FunctionArgExpr::QualifiedWildcard(_object_name) => todo!(), 455 | FunctionArgExpr::Wildcard => todo!(), 456 | }, 457 | }) 458 | .collect::>(), 459 | Expr::Case { 460 | conditions, 461 | results, 462 | else_result, 463 | .. 464 | } => { 465 | let mut cols = conditions 466 | .iter() 467 | .flat_map(|expr| expr_columns(expr)) 468 | .collect::>(); 469 | let results = results.iter().flat_map(|expr| expr_columns(expr)); 470 | cols.extend(results); 471 | if let Some(else_expr) = else_result { 472 | let columns = expr_columns(else_expr.as_ref()); 473 | cols.extend(columns); 474 | } 475 | cols 476 | }, 477 | Expr::UnaryOp { expr, .. } => expr_columns(expr), 478 | expr => todo!("expr_columns rest of the ops {expr}"), 479 | } 480 | } 481 | 482 | pub fn query_table_names(query: &Box) -> Vec<&ObjectName> { 483 | match query.body.as_ref() { 484 | SetExpr::Select(select) => select 485 | .from 486 | .iter() 487 | .map(|table| match &table.relation { 488 | TableFactor::Table { name, .. } => name, 489 | _ => todo!(), 490 | }) 491 | .collect::>(), 492 | SetExpr::Query(query) => query_table_names(query), 493 | _ => todo!("query_table_names"), 494 | } 495 | } 496 | 497 | pub fn placeholder_len(stmt: &Statement) -> usize { 498 | match stmt { 499 | Statement::Insert { source, .. } => match source { 500 | Some(query) => { 501 | let Query { body, .. } = query.as_ref(); 502 | match body.as_ref() { 503 | SetExpr::Values(values) => values 504 | .rows 505 | .iter() 506 | .flat_map(|expr| expr) 507 | .collect::>() 508 | .len(), 509 | SetExpr::Select(select) => { 510 | let Select { selection, .. } = select.as_ref(); 511 | match selection.as_ref() { 512 | Some(expr) => expr_columns(expr) 513 | .iter() 514 | .filter(|col| col.placeholder.is_some()) 515 | .collect::>() 516 | .len(), 517 | None => 0, 518 | } 519 | } 520 | _ => todo!("fn placeholders"), 521 | } 522 | } 523 | None => 0, 524 | }, 525 | Statement::Update { 526 | assignments, 527 | selection, 528 | .. 529 | } => { 530 | let len = assignments.len(); 531 | match selection { 532 | Some(expr) => expr_columns(expr).len() + len, 533 | None => len, 534 | } 535 | } 536 | Statement::Delete { selection, .. } => match selection { 537 | Some(expr) => expr_columns(expr).len(), 538 | None => todo!(), 539 | }, 540 | Statement::Query(query) => { 541 | let Query { body, .. } = query.as_ref(); 542 | match body.as_ref() { 543 | SetExpr::Values(values) => values 544 | .rows 545 | .iter() 546 | .flat_map(|expr| expr) 547 | .collect::>() 548 | .len(), 549 | SetExpr::Select(select) => { 550 | let Select { selection, .. } = select.as_ref(); 551 | match selection.as_ref() { 552 | Some(expr) => expr_columns(expr) 553 | .iter() 554 | .filter(|col| col.placeholder.is_some()) 555 | .collect::>() 556 | .len(), 557 | None => 0, 558 | } 559 | } 560 | _ => todo!("fn placeholders"), 561 | } 562 | } 563 | _ => 0, 564 | } 565 | } 566 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use static_sqlite::{sql, FirstRow, Result, Sqlite}; 2 | 3 | #[tokio::test] 4 | async fn option_type_works() -> Result<()> { 5 | sql! { 6 | let migrate = r#" 7 | create table Row ( 8 | txt text 9 | ) 10 | "#; 11 | 12 | let insert_row = r#" 13 | insert into Row (txt) values (:txt) returning * 14 | "#; 15 | } 16 | 17 | let db = static_sqlite::open(":memory:").await?; 18 | let _k = migrate(&db).await?; 19 | let txt = Some("txt"); 20 | let row = insert_row(&db, txt).await?.first_row()?; 21 | 22 | assert_eq!(row.txt, Some("txt".into())); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[tokio::test] 28 | async fn it_works() -> Result<()> { 29 | sql! { 30 | let migrations = r#" 31 | create table User ( 32 | id integer primary key, 33 | email text not null unique 34 | ); 35 | 36 | create table Row ( 37 | not_null_text text not null, 38 | not_null_integer integer not null, 39 | not_null_real real not null, 40 | not_null_blob blob not null, 41 | null_text text, 42 | null_integer integer, 43 | null_real real, 44 | null_blob blob 45 | ); 46 | 47 | alter table Row add column nullable_text text; 48 | alter table Row add column nullable_integer integer; 49 | alter table Row add column nullable_real real; 50 | alter table Row add column nullable_blob blob; 51 | "#; 52 | 53 | let insert_row = r#" 54 | insert into Row ( 55 | not_null_text, 56 | not_null_integer, 57 | not_null_real, 58 | not_null_blob, 59 | null_text, 60 | null_integer, 61 | null_real, 62 | null_blob, 63 | nullable_text, 64 | nullable_integer, 65 | nullable_real, 66 | nullable_blob 67 | ) 68 | values ( 69 | :not_null_text, 70 | :not_null_integer, 71 | :not_null_real, 72 | :not_null_blob, 73 | :null_text, 74 | :null_integer, 75 | :null_real, 76 | :null_blob, 77 | :nullable_text, 78 | :nullable_integer, 79 | :nullable_real, 80 | :nullable_blob 81 | ) 82 | returning * 83 | "#; 84 | } 85 | 86 | async fn db(path: &str) -> Result { 87 | let sqlite = static_sqlite::open(path).await?; 88 | static_sqlite::execute_all( 89 | &sqlite, 90 | r#" 91 | pragma journal_mode = wal; 92 | pragma synchronous = normal; 93 | pragma foreign_keys = on; 94 | pragma busy_timeout = 5000; 95 | pragma cache_size = -64000; 96 | pragma strict = on; 97 | "#, 98 | ) 99 | .await?; 100 | migrations(&sqlite).await?; 101 | Ok(sqlite) 102 | } 103 | 104 | let db = db(":memory:").await?; 105 | 106 | let row = insert_row( 107 | &db, 108 | "not_null_text", 109 | 1, 110 | 1.0, 111 | vec![0xBE, 0xEF], 112 | None::, 113 | None, 114 | None, 115 | None, 116 | Some("nullable_text"), 117 | Some(2), 118 | Some(2.0), 119 | Some(vec![0xFE, 0xED]), 120 | ) 121 | .await?.first_row()?; 122 | 123 | assert_eq!( 124 | row, 125 | InsertRow { 126 | not_null_text: "not_null_text".into(), 127 | not_null_integer: 1, 128 | not_null_real: 1., 129 | not_null_blob: vec![0xBE, 0xEF], 130 | null_text: None, 131 | null_integer: None, 132 | null_real: None, 133 | null_blob: None, 134 | nullable_text: Some("nullable_text".into()), 135 | nullable_integer: Some(2), 136 | nullable_real: Some(2.), 137 | nullable_blob: Some(vec![0xFE, 0xED]), 138 | } 139 | ); 140 | 141 | Ok(()) 142 | } 143 | 144 | #[tokio::test] 145 | async fn readme_works() -> Result<()> { 146 | sql! { 147 | let migrate = r#" 148 | create table User ( 149 | id integer primary key, 150 | name text unique not null 151 | ); 152 | 153 | alter table User 154 | add column created_at integer; 155 | 156 | alter table User 157 | drop column created_at; 158 | "#; 159 | 160 | let insert_user = r#" 161 | insert into User (name) 162 | values (:name) 163 | returning * 164 | "#; 165 | } 166 | 167 | let db = static_sqlite::open(":memory:").await?; 168 | let _ = migrate(&db).await?; 169 | let user = insert_user(&db, "swlkr").await?.first_row()?; 170 | 171 | assert_eq!(user.id, 1); 172 | assert_eq!(user.name, "swlkr"); 173 | 174 | Ok(()) 175 | } 176 | 177 | #[tokio::test] 178 | async fn crud_works() -> Result<()> { 179 | sql! { 180 | let migrate = r#" 181 | create table User ( 182 | id integer primary key, 183 | name text unique not null 184 | ); 185 | "#; 186 | 187 | let insert_user = r#" 188 | insert into User (name) 189 | values (:name) 190 | returning * 191 | "#; 192 | 193 | let update_user = r#" 194 | update User set name = :name where id = :id returning * 195 | "#; 196 | 197 | let delete_user = r#" 198 | delete from User where id = :id 199 | "#; 200 | 201 | let all_users = r#" 202 | select id, name from User 203 | "#; 204 | } 205 | 206 | let db = static_sqlite::open(":memory:").await?; 207 | let _ = migrate(&db).await?; 208 | let user = insert_user(&db, "swlkr").await?.first_row()?; 209 | assert_eq!(user.id, 1); 210 | assert_eq!(user.name, "swlkr"); 211 | 212 | let users = all_users(&db).await?; 213 | assert_eq!(users.len(), 1); 214 | let user = users.first().unwrap(); 215 | assert_eq!(user.id, 1); 216 | assert_eq!(user.name, "swlkr"); 217 | 218 | let user = update_user(&db, "swlkr2", 1).await?.first_row()?; 219 | assert_eq!(user.id, 1); 220 | assert_eq!(user.name, "swlkr2"); 221 | 222 | delete_user(&db, 1).await?; 223 | let users = all_users(&db).await?; 224 | assert_eq!(users.len(), 0); 225 | 226 | Ok(()) 227 | } 228 | 229 | #[test] 230 | fn ui() { 231 | let t = trybuild::TestCases::new(); 232 | t.compile_fail("tests/ui/*.rs"); 233 | } 234 | -------------------------------------------------------------------------------- /tests/ui/fk_fails.rs: -------------------------------------------------------------------------------- 1 | use static_sqlite::sql; 2 | 3 | sql! { 4 | let migrate = r#" 5 | create table User ( 6 | id integer primary key, 7 | email text unique not null 8 | ); 9 | 10 | create table Todo ( 11 | id integer primary key, 12 | user_id integer not null references Users(id) 13 | ); 14 | "#; 15 | } 16 | -------------------------------------------------------------------------------- /tests/ui/fk_fails.stderr: -------------------------------------------------------------------------------- 1 | error: MissingTable("Users") 2 | --> tests/ui/fk_fails.rs:4:9 3 | | 4 | 4 | let migrate = r#" 5 | | ^^^^^^^ 6 | 7 | error[E0601]: `main` function not found in crate `$CRATE` 8 | --> tests/ui/fk_fails.rs:15:2 9 | | 10 | 15 | } 11 | | ^ consider adding a `main` function to `$DIR/tests/ui/fk_fails.rs` 12 | --------------------------------------------------------------------------------