├── .github └── workflows │ └── main.yml ├── .gitignore ├── .gitmodules ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── VERSION.md ├── build.rs ├── src ├── ast.rs ├── bindings.rs ├── error.rs ├── lib.rs ├── query.rs ├── serde.rs ├── str.rs └── str │ ├── ext.rs │ ├── helpers.rs │ └── nodes.rs └── tests ├── data ├── sql │ ├── func_1.sql │ ├── func_2.sql │ ├── table_1.sql │ └── view_1.sql └── tree │ └── simple_plpgsql.json ├── fingerprint_tests.rs ├── normalize_tests.rs ├── parse_plpgsql_tests.rs ├── parse_tests.rs ├── str_tests.rs └── version_numbers.rs /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | types: [ opened, synchronize, reopened ] 4 | push: 5 | branches: 6 | - master 7 | - "[0-9].x" 8 | 9 | name: Continuous integration 10 | 11 | jobs: 12 | ci: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | rust: 17 | - stable 18 | - beta 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | 25 | - name: Cache cargo registry 26 | uses: actions/cache@v4 27 | with: 28 | path: | 29 | ~/.cargo/registry 30 | ~/.cargo/git 31 | key: ${{ runner.os }}-${{ matrix.backend }}-cargo-${{ hashFiles('**/Cargo.toml') }} 32 | 33 | - name: Install toolchain 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | toolchain: ${{ matrix.rust }} 37 | 38 | - name: Build pg_parse 39 | run: cargo build --all-features 40 | 41 | - name: Run tests 42 | run: cargo test --all-features 43 | 44 | check_style: 45 | name: Check file formatting and style 46 | runs-on: ubuntu-latest 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | submodules: recursive 51 | 52 | - uses: dtolnay/rust-toolchain@stable 53 | with: 54 | toolchain: stable 55 | components: clippy, rustfmt 56 | 57 | - name: Cache cargo registry 58 | uses: actions/cache@v4 59 | with: 60 | path: | 61 | ~/.cargo/registry 62 | ~/.cargo/git 63 | key: clippy-cargo-${{ hashFiles('**/Cargo.toml') }} 64 | 65 | - name: Check file formatting 66 | run: cargo fmt --all -- --check 67 | 68 | - name: Run clippy 69 | run: cargo clippy 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .idea/ 3 | .DS_Store -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/libpg_query"] 2 | path = lib/libpg_query 3 | url = https://github.com/pganalyze/libpg_query 4 | -------------------------------------------------------------------------------- /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 = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "bindgen" 16 | version = "0.71.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" 19 | dependencies = [ 20 | "bitflags", 21 | "cexpr", 22 | "clang-sys", 23 | "itertools", 24 | "log", 25 | "prettyplease", 26 | "proc-macro2", 27 | "quote", 28 | "regex", 29 | "rustc-hash", 30 | "shlex", 31 | "syn", 32 | ] 33 | 34 | [[package]] 35 | name = "bitflags" 36 | version = "2.9.1" 37 | source = "registry+https://github.com/rust-lang/crates.io-index" 38 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 39 | 40 | [[package]] 41 | name = "cexpr" 42 | version = "0.6.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 45 | dependencies = [ 46 | "nom", 47 | ] 48 | 49 | [[package]] 50 | name = "cfg-if" 51 | version = "1.0.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 54 | 55 | [[package]] 56 | name = "clang-sys" 57 | version = "1.8.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 60 | dependencies = [ 61 | "glob", 62 | "libc", 63 | "libloading", 64 | ] 65 | 66 | [[package]] 67 | name = "displaydoc" 68 | version = "0.2.5" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 71 | dependencies = [ 72 | "proc-macro2", 73 | "quote", 74 | "syn", 75 | ] 76 | 77 | [[package]] 78 | name = "either" 79 | version = "1.15.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 82 | 83 | [[package]] 84 | name = "equivalent" 85 | version = "1.0.2" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 88 | 89 | [[package]] 90 | name = "form_urlencoded" 91 | version = "1.2.1" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 94 | dependencies = [ 95 | "percent-encoding", 96 | ] 97 | 98 | [[package]] 99 | name = "glob" 100 | version = "0.3.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 103 | 104 | [[package]] 105 | name = "hashbrown" 106 | version = "0.15.3" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" 109 | 110 | [[package]] 111 | name = "heck" 112 | version = "0.5.0" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 115 | 116 | [[package]] 117 | name = "icu_collections" 118 | version = "2.0.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" 121 | dependencies = [ 122 | "displaydoc", 123 | "potential_utf", 124 | "yoke", 125 | "zerofrom", 126 | "zerovec", 127 | ] 128 | 129 | [[package]] 130 | name = "icu_locale_core" 131 | version = "2.0.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" 134 | dependencies = [ 135 | "displaydoc", 136 | "litemap", 137 | "tinystr", 138 | "writeable", 139 | "zerovec", 140 | ] 141 | 142 | [[package]] 143 | name = "icu_normalizer" 144 | version = "2.0.0" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" 147 | dependencies = [ 148 | "displaydoc", 149 | "icu_collections", 150 | "icu_normalizer_data", 151 | "icu_properties", 152 | "icu_provider", 153 | "smallvec", 154 | "zerovec", 155 | ] 156 | 157 | [[package]] 158 | name = "icu_normalizer_data" 159 | version = "2.0.0" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" 162 | 163 | [[package]] 164 | name = "icu_properties" 165 | version = "2.0.1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" 168 | dependencies = [ 169 | "displaydoc", 170 | "icu_collections", 171 | "icu_locale_core", 172 | "icu_properties_data", 173 | "icu_provider", 174 | "potential_utf", 175 | "zerotrie", 176 | "zerovec", 177 | ] 178 | 179 | [[package]] 180 | name = "icu_properties_data" 181 | version = "2.0.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" 184 | 185 | [[package]] 186 | name = "icu_provider" 187 | version = "2.0.0" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" 190 | dependencies = [ 191 | "displaydoc", 192 | "icu_locale_core", 193 | "stable_deref_trait", 194 | "tinystr", 195 | "writeable", 196 | "yoke", 197 | "zerofrom", 198 | "zerotrie", 199 | "zerovec", 200 | ] 201 | 202 | [[package]] 203 | name = "idna" 204 | version = "1.0.3" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 207 | dependencies = [ 208 | "idna_adapter", 209 | "smallvec", 210 | "utf8_iter", 211 | ] 212 | 213 | [[package]] 214 | name = "idna_adapter" 215 | version = "1.2.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" 218 | dependencies = [ 219 | "icu_normalizer", 220 | "icu_properties", 221 | ] 222 | 223 | [[package]] 224 | name = "indexmap" 225 | version = "2.9.0" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 228 | dependencies = [ 229 | "equivalent", 230 | "hashbrown", 231 | ] 232 | 233 | [[package]] 234 | name = "itertools" 235 | version = "0.13.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 238 | dependencies = [ 239 | "either", 240 | ] 241 | 242 | [[package]] 243 | name = "itoa" 244 | version = "1.0.15" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 247 | 248 | [[package]] 249 | name = "libc" 250 | version = "0.2.172" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 253 | 254 | [[package]] 255 | name = "libloading" 256 | version = "0.8.7" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" 259 | dependencies = [ 260 | "cfg-if", 261 | "windows-targets", 262 | ] 263 | 264 | [[package]] 265 | name = "litemap" 266 | version = "0.8.0" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" 269 | 270 | [[package]] 271 | name = "log" 272 | version = "0.4.27" 273 | source = "registry+https://github.com/rust-lang/crates.io-index" 274 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 275 | 276 | [[package]] 277 | name = "memchr" 278 | version = "2.7.4" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 281 | 282 | [[package]] 283 | name = "minimal-lexical" 284 | version = "0.2.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 287 | 288 | [[package]] 289 | name = "nom" 290 | version = "7.1.3" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 293 | dependencies = [ 294 | "memchr", 295 | "minimal-lexical", 296 | ] 297 | 298 | [[package]] 299 | name = "percent-encoding" 300 | version = "2.3.1" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 303 | 304 | [[package]] 305 | name = "pg_parse" 306 | version = "0.12.0" 307 | dependencies = [ 308 | "bindgen", 309 | "heck", 310 | "regex", 311 | "serde", 312 | "serde_json", 313 | "version-sync", 314 | ] 315 | 316 | [[package]] 317 | name = "potential_utf" 318 | version = "0.1.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" 321 | dependencies = [ 322 | "zerovec", 323 | ] 324 | 325 | [[package]] 326 | name = "prettyplease" 327 | version = "0.2.32" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 330 | dependencies = [ 331 | "proc-macro2", 332 | "syn", 333 | ] 334 | 335 | [[package]] 336 | name = "proc-macro2" 337 | version = "1.0.95" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 340 | dependencies = [ 341 | "unicode-ident", 342 | ] 343 | 344 | [[package]] 345 | name = "pulldown-cmark" 346 | version = "0.9.6" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" 349 | dependencies = [ 350 | "bitflags", 351 | "memchr", 352 | "unicase", 353 | ] 354 | 355 | [[package]] 356 | name = "quote" 357 | version = "1.0.40" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 360 | dependencies = [ 361 | "proc-macro2", 362 | ] 363 | 364 | [[package]] 365 | name = "regex" 366 | version = "1.11.1" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 369 | dependencies = [ 370 | "aho-corasick", 371 | "memchr", 372 | "regex-automata", 373 | "regex-syntax", 374 | ] 375 | 376 | [[package]] 377 | name = "regex-automata" 378 | version = "0.4.9" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 381 | dependencies = [ 382 | "aho-corasick", 383 | "memchr", 384 | "regex-syntax", 385 | ] 386 | 387 | [[package]] 388 | name = "regex-syntax" 389 | version = "0.8.5" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 392 | 393 | [[package]] 394 | name = "rustc-hash" 395 | version = "2.1.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 398 | 399 | [[package]] 400 | name = "ryu" 401 | version = "1.0.20" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 404 | 405 | [[package]] 406 | name = "semver" 407 | version = "1.0.26" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 410 | 411 | [[package]] 412 | name = "serde" 413 | version = "1.0.219" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 416 | dependencies = [ 417 | "serde_derive", 418 | ] 419 | 420 | [[package]] 421 | name = "serde_derive" 422 | version = "1.0.219" 423 | source = "registry+https://github.com/rust-lang/crates.io-index" 424 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 425 | dependencies = [ 426 | "proc-macro2", 427 | "quote", 428 | "syn", 429 | ] 430 | 431 | [[package]] 432 | name = "serde_json" 433 | version = "1.0.140" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 436 | dependencies = [ 437 | "itoa", 438 | "memchr", 439 | "ryu", 440 | "serde", 441 | ] 442 | 443 | [[package]] 444 | name = "serde_spanned" 445 | version = "0.6.8" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 448 | dependencies = [ 449 | "serde", 450 | ] 451 | 452 | [[package]] 453 | name = "shlex" 454 | version = "1.3.0" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 457 | 458 | [[package]] 459 | name = "smallvec" 460 | version = "1.15.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" 463 | 464 | [[package]] 465 | name = "stable_deref_trait" 466 | version = "1.2.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 469 | 470 | [[package]] 471 | name = "syn" 472 | version = "2.0.101" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 475 | dependencies = [ 476 | "proc-macro2", 477 | "quote", 478 | "unicode-ident", 479 | ] 480 | 481 | [[package]] 482 | name = "synstructure" 483 | version = "0.13.2" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" 486 | dependencies = [ 487 | "proc-macro2", 488 | "quote", 489 | "syn", 490 | ] 491 | 492 | [[package]] 493 | name = "tinystr" 494 | version = "0.8.1" 495 | source = "registry+https://github.com/rust-lang/crates.io-index" 496 | checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" 497 | dependencies = [ 498 | "displaydoc", 499 | "zerovec", 500 | ] 501 | 502 | [[package]] 503 | name = "toml" 504 | version = "0.7.8" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" 507 | dependencies = [ 508 | "serde", 509 | "serde_spanned", 510 | "toml_datetime", 511 | "toml_edit", 512 | ] 513 | 514 | [[package]] 515 | name = "toml_datetime" 516 | version = "0.6.9" 517 | source = "registry+https://github.com/rust-lang/crates.io-index" 518 | checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" 519 | dependencies = [ 520 | "serde", 521 | ] 522 | 523 | [[package]] 524 | name = "toml_edit" 525 | version = "0.19.15" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 528 | dependencies = [ 529 | "indexmap", 530 | "serde", 531 | "serde_spanned", 532 | "toml_datetime", 533 | "winnow", 534 | ] 535 | 536 | [[package]] 537 | name = "unicase" 538 | version = "2.8.1" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 541 | 542 | [[package]] 543 | name = "unicode-ident" 544 | version = "1.0.18" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 547 | 548 | [[package]] 549 | name = "url" 550 | version = "2.5.4" 551 | source = "registry+https://github.com/rust-lang/crates.io-index" 552 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 553 | dependencies = [ 554 | "form_urlencoded", 555 | "idna", 556 | "percent-encoding", 557 | ] 558 | 559 | [[package]] 560 | name = "utf8_iter" 561 | version = "1.0.4" 562 | source = "registry+https://github.com/rust-lang/crates.io-index" 563 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 564 | 565 | [[package]] 566 | name = "version-sync" 567 | version = "0.9.5" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "835169da0173ea373ddf5987632aac1f918967fbbe58195e304342282efa6089" 570 | dependencies = [ 571 | "proc-macro2", 572 | "pulldown-cmark", 573 | "regex", 574 | "semver", 575 | "syn", 576 | "toml", 577 | "url", 578 | ] 579 | 580 | [[package]] 581 | name = "windows-targets" 582 | version = "0.53.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" 585 | dependencies = [ 586 | "windows_aarch64_gnullvm", 587 | "windows_aarch64_msvc", 588 | "windows_i686_gnu", 589 | "windows_i686_gnullvm", 590 | "windows_i686_msvc", 591 | "windows_x86_64_gnu", 592 | "windows_x86_64_gnullvm", 593 | "windows_x86_64_msvc", 594 | ] 595 | 596 | [[package]] 597 | name = "windows_aarch64_gnullvm" 598 | version = "0.53.0" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" 601 | 602 | [[package]] 603 | name = "windows_aarch64_msvc" 604 | version = "0.53.0" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" 607 | 608 | [[package]] 609 | name = "windows_i686_gnu" 610 | version = "0.53.0" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" 613 | 614 | [[package]] 615 | name = "windows_i686_gnullvm" 616 | version = "0.53.0" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" 619 | 620 | [[package]] 621 | name = "windows_i686_msvc" 622 | version = "0.53.0" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" 625 | 626 | [[package]] 627 | name = "windows_x86_64_gnu" 628 | version = "0.53.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" 631 | 632 | [[package]] 633 | name = "windows_x86_64_gnullvm" 634 | version = "0.53.0" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" 637 | 638 | [[package]] 639 | name = "windows_x86_64_msvc" 640 | version = "0.53.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" 643 | 644 | [[package]] 645 | name = "winnow" 646 | version = "0.5.40" 647 | source = "registry+https://github.com/rust-lang/crates.io-index" 648 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 649 | dependencies = [ 650 | "memchr", 651 | ] 652 | 653 | [[package]] 654 | name = "writeable" 655 | version = "0.6.1" 656 | source = "registry+https://github.com/rust-lang/crates.io-index" 657 | checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" 658 | 659 | [[package]] 660 | name = "yoke" 661 | version = "0.8.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" 664 | dependencies = [ 665 | "serde", 666 | "stable_deref_trait", 667 | "yoke-derive", 668 | "zerofrom", 669 | ] 670 | 671 | [[package]] 672 | name = "yoke-derive" 673 | version = "0.8.0" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" 676 | dependencies = [ 677 | "proc-macro2", 678 | "quote", 679 | "syn", 680 | "synstructure", 681 | ] 682 | 683 | [[package]] 684 | name = "zerofrom" 685 | version = "0.1.6" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" 688 | dependencies = [ 689 | "zerofrom-derive", 690 | ] 691 | 692 | [[package]] 693 | name = "zerofrom-derive" 694 | version = "0.1.6" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" 697 | dependencies = [ 698 | "proc-macro2", 699 | "quote", 700 | "syn", 701 | "synstructure", 702 | ] 703 | 704 | [[package]] 705 | name = "zerotrie" 706 | version = "0.2.2" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" 709 | dependencies = [ 710 | "displaydoc", 711 | "yoke", 712 | "zerofrom", 713 | ] 714 | 715 | [[package]] 716 | name = "zerovec" 717 | version = "0.11.2" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" 720 | dependencies = [ 721 | "yoke", 722 | "zerofrom", 723 | "zerovec-derive", 724 | ] 725 | 726 | [[package]] 727 | name = "zerovec-derive" 728 | version = "0.11.1" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" 731 | dependencies = [ 732 | "proc-macro2", 733 | "quote", 734 | "syn", 735 | ] 736 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pg_parse" 3 | description = "PostgreSQL parser that uses the actual PostgreSQL server source to parse SQL queries and return the internal PostgreSQL parse tree." 4 | version = "0.12.0" 5 | authors = ["Paul Mason "] 6 | edition = "2024" 7 | documentation = "https://docs.rs/pg_parse/" 8 | build = "build.rs" 9 | license = "MIT" 10 | readme = "./README.md" 11 | repository = "https://github.com/paupino/pg_parse" 12 | 13 | [features] 14 | default = [] 15 | str = [] # Enable converting nodes back into strings 16 | 17 | [dependencies] 18 | serde = { version = "1", features = ["derive"] } 19 | serde_json = "1" 20 | 21 | [dev-dependencies] 22 | regex = "1.7" 23 | version-sync = "0.9" 24 | 25 | [build-dependencies] 26 | bindgen = "0.71" 27 | heck = "0.5" 28 | serde = { version = "1", features = ["derive"] } 29 | serde_json = "1" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Paul Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pg_parse   [![Build Status]][actions] [![Latest Version]][crates.io] [![Docs Badge]][docs] 2 | =========== 3 | 4 | [Build Status]: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fpaupino%2Fpg_parse%2Fbadge&label=build&logo=none 5 | 6 | [actions]: https://actions-badge.atrox.dev/paupino/pg_parse/goto 7 | 8 | [Latest Version]: https://img.shields.io/crates/v/pg_parse.svg 9 | 10 | [crates.io]: https://crates.io/crates/pg_parse 11 | 12 | [Docs Badge]: https://docs.rs/pg_parse/badge.svg 13 | 14 | [docs]: https://docs.rs/pg_parse 15 | 16 | PostgreSQL parser for Rust that uses the [actual PostgreSQL server source]((https://github.com/pganalyze/libpg_query)) 17 | to parse 18 | SQL queries and return the internal PostgreSQL parse tree. 19 | 20 | ## Getting started 21 | 22 | Add the following to your `Cargo.toml` 23 | 24 | ```toml 25 | [dependencies] 26 | pg_parse = "0.12" 27 | ``` 28 | 29 | ## Example: Parsing a query 30 | 31 | ```rust 32 | use pg_parse::ast::Node; 33 | 34 | let result = pg_parse::parse("SELECT * FROM contacts"); 35 | assert!(result.is_ok()); 36 | let result = result.unwrap(); 37 | assert!(matches!(*&result[0], Node::SelectStmt(_))); 38 | 39 | // We can also convert back to a string, if the `str` feature is enabled (enabled by default). 40 | #[cfg(feature = "str")] 41 | assert_eq!(result[0].to_string(), "SELECT * FROM contacts"); 42 | ``` 43 | 44 | ## What's the difference between pg_parse and pg_query.rs? 45 | 46 | The [`pganalyze`](https://github.com/pganalyze/) organization maintains the official implementation: [ 47 | `pg_query.rs`](https://github.com/pganalyze/pg_query.rs). This 48 | closely resembles the name of the C library also published by the team (`libpg_query`). This implementation uses the 49 | protobuf 50 | interface introduced with version 13 of `libpg_query`. 51 | 52 | This library similarly consumes `libpg_query` however utilizes the older JSON interface to manage parsing. The intention 53 | of this library 54 | is to maintain a dependency "light" implementation with `serde` and `serde_json` being the only required runtime 55 | dependencies. 56 | 57 | So which one should you use? You probably want to use the official `pg_query.rs` library as that will continue to be 58 | kept closely up to date with `libpg_query` updates. This library will continue to be maintained however may not be as 59 | up-to-date as the official implementation. 60 | 61 | ## Credits 62 | 63 | A huge thank you to [Lukas Fittl](https://github.com/lfittl) for all of his amazing work 64 | creating [libpg_query](https://github.com/pganalyze/libpg_query). 65 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | # Version 0.12 2 | 3 | **This release upgrades `libpg_query` so contains breaking changes.** 4 | 5 | Modified: 6 | 7 | * Updated `libpg_query` to [17-6.1.0](https://github.com/pganalyze/libpg_query/tree/17-6.1.0). 8 | * Made string generation opt-in instead of enabled by default. 9 | * Updated dependencies. 10 | 11 | Other: 12 | 13 | * Bumped the project version to `2024` and updated syntax accordingly. 14 | 15 | # Version 0.11 16 | 17 | **This release upgrades `libpg_query` so contains breaking changes.** 18 | 19 | New: 20 | 21 | * Introduced a `parse_debug` function to allow for consuming functions to inspect the raw JSON output from 22 | `libpg_query`. 23 | This is likely only used internally by library authors, but a useful feature nonetheless. 24 | 25 | Modified: 26 | 27 | * Updated `libpg_query` to [15-4.2.2](https://github.com/pganalyze/libpg_query/tree/15-4.2.2). This required a lot of 28 | refactoring to support the modified 29 | AST being generated. 30 | * String generation is now feature gated under `str`. This feature is not feature complete 31 | so should be used with caution. Please note, this is currently enabled by default. 32 | 33 | Other: 34 | 35 | * Bumped the project version to `2021` and updated syntax accordingly. 36 | 37 | Please note that some syntax support has been dropped between Postgres version releases. For example, 38 | the `?` placeholder is no longer supported. For a full list, please see the `libpg_query` changelog. 39 | 40 | # Version 0.10 41 | 42 | Modified: 43 | 44 | * Updated `libpg_query` to [13-2.2.0](https://github.com/pganalyze/libpg_query/releases/tag/13-2.2.0). 45 | * Build optimization to prevent rebuilding when no changes [#16](https://github.com/paupino/pg_parse/pull/16). 46 | 47 | Thank you [@haileys](https://github.com/haileys) for your contribution! 48 | 49 | # Version 0.9.1 50 | 51 | Modified: 52 | 53 | * Updated `regex` library to remove potential security vulnerability. 54 | 55 | # Version 0.9.0 56 | 57 | Modified: 58 | 59 | * Updated to latest `libpg_query` version which fixes some memory leaks. 60 | * Removed `clippy` build dependency which was subject to a potential security vulnerability. 61 | 62 | # Version 0.8.0 63 | 64 | New: 65 | 66 | * `to_string` functionality for AST allowing you to turn the parsed tree back into SQL. 67 | 68 | # Version 0.7.0 69 | 70 | Renamed project from `pg_query.rs` to `pg_parse`. Going forward the `pganalyze` team will maintain the official fork 71 | leveraging protobuf whereas this library will continue to use the JSON subsystem. 72 | 73 | * Remove `Expr` from generated output since it is a generic superclass. 74 | 75 | # Version 0.6.0 76 | 77 | Fixes issue when parsing some statements that would contain trailing null objects in an array. Deserialization of these 78 | is now performed correctly. Note that this may cause some differing behavior from other `libpg_query` implementations 79 | whereby a "null object" is intended to indicate the end of an array. 80 | 81 | An example of this behaviour is `SELECT DISTINCT a, b FROM c`. The `distinct_clause` generates `[{}]` from 82 | `libpg_query`. 83 | `pg_query.rs` now parses this as `vec![]`. 84 | 85 | # Version 0.5.0 86 | 87 | * Enums can now be compared directly. 88 | * `Null` is generated with empty parameters to support JSON mapping. 89 | 90 | # Version 0.4.0 91 | 92 | Updates `libpg_query` dependency to [`13-2.1.0`](https://github.com/pganalyze/libpg_query/tree/13-2.1.0). 93 | 94 | # Version 0.3.0 95 | 96 | * Fixes `Value` parsing in some situations such as for `typemod` 97 | 98 | # Version 0.2.0 99 | 100 | * Adds in the `List` node type by generating `nodes/pg_list` in `structdef`. 101 | * Implement `std::error::Error` for `Error` type -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use heck::ToSnakeCase; 2 | 3 | use std::collections::{HashMap, HashSet}; 4 | use std::env; 5 | use std::fs::{self, File}; 6 | use std::io::{BufReader, BufWriter, Write}; 7 | use std::path::{Path, PathBuf}; 8 | use std::process::{Command, Stdio}; 9 | 10 | fn main() { 11 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 12 | let build_dir = out_dir.join("libpg_query"); 13 | let src_dir = PathBuf::from("./lib/libpg_query").canonicalize().unwrap(); 14 | println!( 15 | "cargo:rerun-if-changed={}", 16 | src_dir.join("pg_query.h").display() 17 | ); 18 | 19 | // Copy the files over 20 | eprintln!("Copying {} -> {}", src_dir.display(), build_dir.display()); 21 | let changed = copy_dir(&src_dir, &build_dir).expect("Copy failed"); 22 | 23 | // Generate the AST first 24 | generate_ast(&build_dir, &out_dir).expect("AST generation"); 25 | 26 | // Now compile the C library. 27 | // We try to optimize the build a bit by only rebuilding if the directory tree has a detected change 28 | if changed { 29 | let mut make = Command::new("make"); 30 | make.env_remove("PROFILE").arg("-C").arg(&build_dir); 31 | if env::var("PROFILE").unwrap() == "debug" { 32 | make.arg("DEBUG=1"); 33 | } 34 | let status = make 35 | .stdin(Stdio::null()) 36 | .stdout(Stdio::inherit()) 37 | .stderr(Stdio::inherit()) 38 | .status() 39 | .unwrap(); 40 | assert!(status.success()); 41 | } 42 | 43 | // Also generate bindings 44 | let bindings = bindgen::Builder::default() 45 | .header(build_dir.join("pg_query.h").to_str().unwrap()) 46 | .generate() 47 | .expect("Unable to generate bindings"); 48 | 49 | bindings 50 | .write_to_file(out_dir.join("bindings.rs")) 51 | .expect("Couldn't write bindings!"); 52 | 53 | println!("cargo:rustc-link-search=native={}", build_dir.display()); 54 | println!("cargo:rustc-link-lib=static=pg_query"); 55 | } 56 | 57 | fn copy_dir, V: AsRef>(from: U, to: V) -> std::io::Result { 58 | let mut stack = vec![PathBuf::from(from.as_ref())]; 59 | 60 | let output_root = PathBuf::from(to.as_ref()); 61 | let input_root = PathBuf::from(from.as_ref()).components().count(); 62 | 63 | let mut changed = false; 64 | while let Some(working_path) = stack.pop() { 65 | // Generate a relative path 66 | let src: PathBuf = working_path.components().skip(input_root).collect(); 67 | 68 | // Create a destination if missing 69 | let dest = if src.components().count() == 0 { 70 | output_root.clone() 71 | } else { 72 | output_root.join(&src) 73 | }; 74 | if fs::metadata(&dest).is_err() { 75 | fs::create_dir_all(&dest)?; 76 | } 77 | 78 | for entry in fs::read_dir(working_path)? { 79 | let entry = entry?; 80 | let path = entry.path(); 81 | eprintln!("{}", path.display()); 82 | if path.is_dir() { 83 | stack.push(path); 84 | } else if let Some(filename) = path.file_name() { 85 | let dest_path = dest.join(filename); 86 | if dest_path.exists() { 87 | if let Ok(source) = path.metadata() { 88 | if let Ok(dest) = dest_path.metadata() { 89 | if source.len() == dest.len() { 90 | if let Ok(smtime) = source.modified() { 91 | if let Ok(dmtime) = dest.modified() { 92 | if smtime == dmtime { 93 | continue; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | fs::copy(&path, &dest_path)?; 102 | changed = true; 103 | } 104 | } 105 | } 106 | 107 | Ok(changed) 108 | } 109 | 110 | #[derive(serde::Deserialize)] 111 | pub struct Struct { 112 | pub fields: Vec, 113 | pub comment: Option, 114 | } 115 | 116 | #[derive(serde::Deserialize)] 117 | pub struct Field { 118 | pub name: Option, 119 | pub c_type: Option, 120 | pub comment: Option, 121 | } 122 | 123 | #[derive(serde::Deserialize)] 124 | pub struct Enum { 125 | pub values: Vec, 126 | pub comment: Option, 127 | } 128 | 129 | #[derive(serde::Deserialize)] 130 | pub struct EnumValue { 131 | pub name: Option, 132 | pub comment: Option, 133 | pub value: Option, 134 | } 135 | 136 | #[derive(serde::Deserialize)] 137 | pub struct TypeDef { 138 | new_type_name: String, 139 | source_type: String, 140 | comment: Option, 141 | } 142 | 143 | fn generate_ast(build_dir: &Path, out_dir: &Path) -> std::io::Result<()> { 144 | let srcdata_dir = build_dir.join("srcdata"); 145 | assert!( 146 | srcdata_dir.exists(), 147 | "srcdata_dir did not exist: {}", 148 | srcdata_dir.display() 149 | ); 150 | 151 | // Common out dir 152 | let out_file = File::create(out_dir.join("ast.rs"))?; 153 | let mut out_file = BufWriter::new(out_file); 154 | 155 | // Keep track of types for type resolution 156 | let mut type_resolver = TypeResolver::new(); 157 | 158 | // Read in all "Node" types as this helps generating struct vs node 159 | let node_types = File::open(srcdata_dir.join("nodetypes.json"))?; 160 | let node_types = BufReader::new(node_types); 161 | let mut node_types: Vec = serde_json::from_reader(node_types)?; 162 | node_types.push("JsonTablePlan".into()); 163 | for ty in node_types.iter() { 164 | type_resolver.add_node(ty); 165 | } 166 | let node_types = node_types.into_iter().collect(); 167 | 168 | // Generate type aliases first 169 | let type_defs = File::open(srcdata_dir.join("typedefs.json"))?; 170 | let type_defs = BufReader::new(type_defs); 171 | let type_defs: Vec = serde_json::from_reader(type_defs)?; 172 | for ty in type_defs.iter() { 173 | type_resolver.add_alias(&ty.new_type_name, &ty.source_type); 174 | } 175 | make_aliases(&mut out_file, &type_defs, &node_types, &mut type_resolver)?; 176 | 177 | // Enums 178 | let enum_defs = File::open(srcdata_dir.join("enum_defs.json"))?; 179 | let enum_defs = BufReader::new(enum_defs); 180 | let enum_defs: HashMap> = serde_json::from_reader(enum_defs)?; 181 | for map in enum_defs.values() { 182 | for ty in map.keys() { 183 | type_resolver.add_type(ty); 184 | } 185 | } 186 | make_enums(&mut out_file, &enum_defs)?; 187 | 188 | // Structs 189 | let struct_defs = File::open(srcdata_dir.join("struct_defs.json"))?; 190 | let struct_defs = BufReader::new(struct_defs); 191 | let struct_defs: HashMap> = 192 | serde_json::from_reader(struct_defs)?; 193 | for map in struct_defs.values() { 194 | for ty in map.keys() { 195 | if !type_resolver.contains(ty) { 196 | type_resolver.add_type(ty); 197 | } 198 | } 199 | } 200 | 201 | // Finally make the nodes and the primitives 202 | make_nodes(&mut out_file, &struct_defs, &node_types, &type_resolver)?; 203 | Ok(()) 204 | } 205 | 206 | fn make_aliases( 207 | out: &mut BufWriter, 208 | type_defs: &[TypeDef], 209 | node_types: &HashSet, 210 | type_resolver: &mut TypeResolver, 211 | ) -> std::io::Result<()> { 212 | const IGNORE: [&str; 6] = [ 213 | "BlockId", 214 | "ExpandedObjectHeader", 215 | "Name", 216 | "ParallelVacuumState", 217 | "ParamListInfo", 218 | "VacAttrStatsP", 219 | ]; 220 | 221 | for def in type_defs { 222 | if IGNORE.iter().any(|e| def.new_type_name.eq(e)) { 223 | continue; 224 | } 225 | if node_types.contains(&def.source_type) { 226 | // Type alias won't work, so just ignore this and replace the type 227 | type_resolver.add_node(&def.new_type_name); 228 | continue; 229 | } 230 | if let Some(comment) = &def.comment { 231 | writeln!(out, "{}", comment)?; 232 | } 233 | let ty = match &def.source_type[..] { 234 | "char" => "char", 235 | "double" => "f64", 236 | "int16" => "i16", 237 | "int" => "i32", 238 | "signed int" => "i32", 239 | "uint32" => "u32", 240 | "uint64" => "u64", 241 | "unsigned int" => "u32", 242 | "uintptr_t" => "usize", 243 | 244 | "BlockIdData" => "BlockIdData", 245 | "NameData" => "NameData", 246 | "Oid" => "Oid", 247 | "OpExpr" => "OpExpr", 248 | "ParamListInfoData" => "ParamListInfoData", 249 | "regproc" => "regproc", 250 | "TransactionId" => "TransactionId", 251 | "VacAttrStats" => "VacAttrStats", 252 | 253 | unexpected => panic!("Unrecognized type for alias: {}", unexpected), 254 | }; 255 | writeln!(out, "pub type {} = {};", def.new_type_name, ty)?; 256 | type_resolver.add_type(&def.new_type_name); 257 | } 258 | Ok(()) 259 | } 260 | 261 | fn make_enums( 262 | out: &mut BufWriter, 263 | enum_defs: &HashMap>, 264 | ) -> std::io::Result<()> { 265 | const SECTIONS: [&str; 4] = [ 266 | "nodes/parsenodes", 267 | "nodes/primnodes", 268 | "nodes/lockoptions", 269 | "nodes/nodes", 270 | ]; 271 | for section in &SECTIONS { 272 | let map = &enum_defs[*section]; 273 | let mut map = map.iter().collect::>(); 274 | map.sort_by_key(|x| x.0); 275 | 276 | for (name, def) in map { 277 | writeln!( 278 | out, 279 | "#[derive(Copy, Clone, Eq, PartialEq, Debug, serde::Deserialize)]" 280 | )?; 281 | writeln!(out, "pub enum {} {{", name)?; 282 | // This enum has duplicate values - I don't think these are really necessary 283 | let ignore_value = name.eq("PartitionRangeDatumKind"); 284 | 285 | for value in &def.values { 286 | if let Some(comment) = &value.comment { 287 | writeln!(out, " {}", comment)?; 288 | } 289 | if let Some(name) = &value.name { 290 | if ignore_value { 291 | writeln!(out, " {},", name)?; 292 | } else if let Some(v) = &value.value { 293 | writeln!(out, " {} = {},", name, *v)?; 294 | } else { 295 | writeln!(out, " {},", name)?; 296 | } 297 | } 298 | } 299 | writeln!(out, "}}")?; 300 | writeln!(out)?; 301 | } 302 | } 303 | Ok(()) 304 | } 305 | 306 | fn make_nodes( 307 | out: &mut BufWriter, 308 | struct_defs: &HashMap>, 309 | node_types: &HashSet, 310 | type_resolver: &TypeResolver, 311 | ) -> std::io::Result<()> { 312 | const SECTIONS: [&str; 3] = ["nodes/parsenodes", "nodes/primnodes", "nodes/pg_list"]; 313 | const IGNORE: [&str; 1] = [ 314 | "Expr", // Generic Superclass - never constructed directly. 315 | ]; 316 | let mut added = Vec::new(); 317 | 318 | writeln!(out, "#[derive(Debug, serde::Deserialize)]")?; 319 | writeln!(out, "pub enum Node {{")?; 320 | 321 | for section in &SECTIONS { 322 | let map = &struct_defs[*section]; 323 | let mut map = map.iter().collect::>(); 324 | map.sort_by_key(|x| x.0); 325 | 326 | for (name, def) in map { 327 | if IGNORE.iter().any(|x| name.eq(x)) { 328 | continue; 329 | } 330 | 331 | // Only generate node types 332 | if !node_types.contains(name) { 333 | // We panic here since all structs are nodes for our purposes 334 | panic!("Unexpected struct `{}` (not a node).", name); 335 | } 336 | added.push((name, true)); 337 | 338 | // If no fields just generate an empty variant 339 | if def.fields.is_empty() { 340 | writeln!(out, " {},", name)?; 341 | continue; 342 | } 343 | 344 | // Generate with a passable struct 345 | writeln!(out, " {}({}),", name, name)?; 346 | } 347 | } 348 | 349 | // Also do "Value" type nodes. These are generated differently. 350 | writeln!(out, " // Value nodes")?; 351 | let values = &struct_defs["nodes/value"]; 352 | let mut values = values.iter().collect::>(); 353 | values.sort_by_key(|x| x.0); 354 | 355 | for (name, def) in values { 356 | added.push((name, false)); 357 | if def.fields.is_empty() { 358 | writeln!(out, " {} {{ }},", name)?; 359 | continue; 360 | } 361 | 362 | // If this is an A_Const we handle this specially 363 | if name.eq("A_Const") { 364 | writeln!(out, " {name}(ConstValue),")?; 365 | continue; 366 | } 367 | 368 | // These may have many fields, though we may want to handle that explicitly 369 | writeln!(out, " {} {{", name)?; 370 | for field in &def.fields { 371 | let field_name = field.name.as_ref().unwrap(); 372 | let resolved_type = type_resolver.resolve(field.c_type.as_ref().unwrap()); 373 | writeln!(out, " #[serde(default)]")?; 374 | // We force each of these as an Option so we can be explicit about when we 375 | // want to handle absence of a field. 376 | if resolved_type.starts_with("Option<") { 377 | writeln!(out, " {field_name}: {resolved_type},")?; 378 | } else { 379 | writeln!(out, " {field_name}: Option<{resolved_type}>,")?; 380 | } 381 | } 382 | writeln!(out, " }},")?; 383 | } 384 | 385 | writeln!(out, "}}")?; 386 | 387 | // Generate the structs 388 | for section in &SECTIONS { 389 | let map = &struct_defs[*section]; 390 | let mut map = map.iter().collect::>(); 391 | map.sort_by_key(|x| x.0); 392 | 393 | for (name, def) in map { 394 | if IGNORE.iter().any(|x| name.eq(x)) { 395 | continue; 396 | } 397 | 398 | writeln!(out)?; 399 | writeln!(out, "#[derive(Debug, serde::Deserialize)]")?; 400 | writeln!(out, "pub struct {} {{", name)?; 401 | 402 | for field in &def.fields { 403 | let (name, c_type) = match (&field.name, &field.c_type) { 404 | (Some(name), Some(c_type)) => (name, c_type), 405 | _ => continue, 406 | }; 407 | 408 | // These are meta data fields and have no real use 409 | if name == "type" || name == "xpr" { 410 | continue; 411 | } 412 | 413 | // Extract everything needed to build the serde types 414 | let variable_name = if is_reserved(name) { 415 | format!("{}_", name) 416 | } else { 417 | name.to_snake_case() 418 | }; 419 | write!(out, " #[serde(")?; 420 | let mut has_data = false; 421 | if variable_name.ne(name) { 422 | write!(out, "rename = \"{}\"", name)?; 423 | has_data = true; 424 | } 425 | if let Some((deserializer, optional)) = TypeResolver::custom_deserializer(c_type) { 426 | if has_data { 427 | write!(out, ", ")?; 428 | } 429 | write!( 430 | out, 431 | "deserialize_with = \"{}\"{}", 432 | deserializer, 433 | if optional { ", default" } else { "" } 434 | )?; 435 | } else if type_resolver.is_optional(c_type) { 436 | if has_data { 437 | write!(out, ", ")?; 438 | } 439 | write!(out, "default")?; 440 | } 441 | writeln!(out, ")]")?; 442 | writeln!( 443 | out, 444 | " pub {}: {},", 445 | variable_name, 446 | type_resolver.resolve(c_type) 447 | )?; 448 | } 449 | 450 | writeln!(out, "}}")?; 451 | } 452 | } 453 | 454 | // Generate a helpful "to_string" 455 | writeln!(out, "impl Node {{")?; 456 | writeln!(out, " pub fn name(&self) -> &'static str {{")?; 457 | writeln!(out, " match self {{")?; 458 | for (variant, is_struct) in added { 459 | let modifier = if is_struct { "(_)" } else { "{ .. }" }; 460 | writeln!( 461 | out, 462 | " Node::{variant}{modifier} => \"{variant}\",", 463 | )?; 464 | } 465 | writeln!(out, " }}")?; 466 | writeln!(out, " }}")?; 467 | writeln!(out, "}}")?; 468 | 469 | Ok(()) 470 | } 471 | 472 | fn is_reserved(variable: &str) -> bool { 473 | matches!( 474 | variable, 475 | "abstract" 476 | | "become" 477 | | "box" 478 | | "do" 479 | | "final" 480 | | "macro" 481 | | "override" 482 | | "priv" 483 | | "try" 484 | | "typeof" 485 | | "unsized" 486 | | "virtual" 487 | | "yield" 488 | ) 489 | } 490 | 491 | struct TypeResolver { 492 | aliases: HashMap, // bool = primitive 493 | primitive: HashMap<&'static str, &'static str>, 494 | nodes: HashSet, 495 | types: HashSet, 496 | } 497 | 498 | impl TypeResolver { 499 | pub fn new() -> Self { 500 | let mut primitive = HashMap::new(); 501 | primitive.insert("uint32", "u32"); 502 | primitive.insert("uint64", "u64"); 503 | primitive.insert("bits32", "bits32"); // Alias 504 | primitive.insert("bool", "bool"); 505 | primitive.insert("int", "i32"); 506 | primitive.insert("long", "i64"); 507 | primitive.insert("int32", "i32"); 508 | primitive.insert("char*", "Option"); // Make all strings optional 509 | primitive.insert("int16", "i16"); 510 | primitive.insert("char", "char"); 511 | primitive.insert("double", "f64"); 512 | primitive.insert("signed int", "i32"); 513 | primitive.insert("unsigned int", "u32"); 514 | primitive.insert("uintptr_t", "usize"); 515 | 516 | // Similar to primitives 517 | primitive.insert("List*", "Option>"); 518 | primitive.insert("[]Node", "Vec"); 519 | primitive.insert("Node*", "Option>"); 520 | primitive.insert("Expr*", "Option>"); 521 | primitive.insert("String*", "Option"); 522 | primitive.insert("RelFileNumber", "RelFileNumber"); 523 | 524 | // Bitmapset is defined in bitmapset.h and is roughly equivalent to a vector of u32's. 525 | primitive.insert("Bitmapset*", "Option>"); 526 | 527 | TypeResolver { 528 | primitive, 529 | 530 | aliases: HashMap::new(), 531 | nodes: HashSet::new(), 532 | types: HashSet::new(), 533 | } 534 | } 535 | 536 | pub fn add_alias(&mut self, ty: &str, target: &str) { 537 | self.aliases 538 | .insert(ty.to_string(), self.primitive.contains_key(target)); 539 | } 540 | 541 | pub fn add_node(&mut self, ty: &str) { 542 | self.nodes.insert(ty.to_string()); 543 | } 544 | 545 | pub fn add_type(&mut self, ty: &str) { 546 | self.types.insert(ty.to_string()); 547 | } 548 | 549 | pub fn contains(&self, ty: &str) -> bool { 550 | self.aliases.contains_key(ty) 551 | || self.primitive.contains_key(ty) 552 | || self.nodes.contains(ty) 553 | || self.types.contains(ty) 554 | } 555 | 556 | pub fn is_primitive(&self, ty: &str) -> bool { 557 | self.primitive.contains_key(ty) || self.aliases.get(ty).copied().unwrap_or_default() 558 | } 559 | 560 | pub fn is_optional(&self, ty: &str) -> bool { 561 | self.is_primitive(ty) || ty.ends_with('*') 562 | } 563 | 564 | pub fn custom_deserializer(ty: &str) -> Option<(&str, bool)> { 565 | match ty { 566 | "[]Node" => Some(("crate::serde::deserialize_node_array", false)), 567 | "List*" => Some(("crate::serde::deserialize_node_array_opt", true)), 568 | "String*" => Some(("crate::serde::deserialize_nested_string_opt", true)), 569 | _ => None, 570 | } 571 | } 572 | 573 | pub fn resolve(&self, c_type: &str) -> String { 574 | if let Some(ty) = self.primitive.get(c_type) { 575 | return ty.to_string(); 576 | } 577 | if let Some(ty) = c_type.strip_suffix('*') { 578 | if let Some(primitive) = self.aliases.get(c_type) { 579 | return if *primitive { 580 | format!("Option<{}>", ty) 581 | } else { 582 | format!("Option>", ty) 583 | }; 584 | } 585 | 586 | if self.nodes.contains(ty) || self.types.contains(ty) { 587 | return format!("Option>", ty); 588 | } 589 | } else { 590 | if let Some(primitive) = self.aliases.get(c_type) { 591 | return if *primitive { 592 | c_type.to_string() 593 | } else { 594 | format!("Box<{}>", c_type) 595 | }; 596 | } 597 | 598 | if self.nodes.contains(c_type) || self.types.contains(c_type) { 599 | return format!("Box<{}>", c_type); 600 | } 601 | } 602 | 603 | // SHOULD be unreachable 604 | // let mut expected = String::new(); 605 | // for ty in self.types.keys() { 606 | // expected.push_str(ty); 607 | // expected.push(','); 608 | // } 609 | unreachable!("Unexpected type: {}", c_type) 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /src/ast.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(unused)] 5 | #![allow(clippy::all)] 6 | 7 | use serde::Deserializer; 8 | 9 | // Type aliases 10 | pub type bits32 = u32; 11 | pub type RelFileNumber = Oid; 12 | 13 | // Generated types 14 | include!(concat!(env!("OUT_DIR"), "/ast.rs")); 15 | 16 | #[derive(Debug, serde::Deserialize)] 17 | pub struct Value(pub Node); 18 | 19 | #[derive(Debug, Clone, PartialEq)] 20 | pub enum ConstValue { 21 | Bool(bool), 22 | Integer(i64), 23 | Float(String), 24 | String(String), 25 | BitString(String), 26 | Null, 27 | NotNull, 28 | } 29 | 30 | impl ConstValue { 31 | pub fn name(&self) -> &'static str { 32 | match self { 33 | ConstValue::Bool(_) => "ConstBool", 34 | ConstValue::Integer(_) => "ConstInteger", 35 | ConstValue::Float(_) => "ConstFloat", 36 | ConstValue::String(_) => "ConstString", 37 | ConstValue::BitString(_) => "ConstBitString", 38 | ConstValue::Null => "ConstNull", 39 | ConstValue::NotNull => "ConstNotNull", 40 | } 41 | } 42 | } 43 | 44 | impl Value { 45 | pub fn inner(&self) -> &Node { 46 | &self.0 47 | } 48 | } 49 | 50 | pub(crate) mod constants { 51 | // FrameOptions is an OR of these bits. The NONDEFAULT and BETWEEN bits are 52 | // used so that ruleutils.c can tell which properties were specified and 53 | // which were defaulted; the correct behavioral bits must be set either way. 54 | // The START_foo and END_foo options must come in pairs of adjacent bits for 55 | // the convenience of gram.y, even though some of them are useless/invalid. 56 | /// any specified? 57 | pub const FRAMEOPTION_NONDEFAULT: i32 = 0x00001; 58 | /// RANGE behavior 59 | pub const FRAMEOPTION_RANGE: i32 = 0x00002; 60 | /// ROWS behavior 61 | pub const FRAMEOPTION_ROWS: i32 = 0x00004; 62 | /// GROUPS behavior 63 | pub const FRAMEOPTION_GROUPS: i32 = 0x00008; 64 | /// BETWEEN given? 65 | pub const FRAMEOPTION_BETWEEN: i32 = 0x00010; 66 | /// start is U. P. 67 | pub const FRAMEOPTION_START_UNBOUNDED_PRECEDING: i32 = 0x00020; 68 | /// (disallowed) 69 | pub const FRAMEOPTION_END_UNBOUNDED_PRECEDING: i32 = 0x00040; 70 | /// (disallowed) 71 | pub const FRAMEOPTION_START_UNBOUNDED_FOLLOWING: i32 = 0x00080; 72 | /// end is U. F. 73 | pub const FRAMEOPTION_END_UNBOUNDED_FOLLOWING: i32 = 0x00100; 74 | /// start is C. R. 75 | pub const FRAMEOPTION_START_CURRENT_ROW: i32 = 0x00200; 76 | /// end is C. R. 77 | pub const FRAMEOPTION_END_CURRENT_ROW: i32 = 0x00400; 78 | /// start is O. P. 79 | pub const FRAMEOPTION_START_OFFSET_PRECEDING: i32 = 0x00800; 80 | /// end is O. P. 81 | pub const FRAMEOPTION_END_OFFSET_PRECEDING: i32 = 0x01000; 82 | /// start is O. F. 83 | pub const FRAMEOPTION_START_OFFSET_FOLLOWING: i32 = 0x02000; 84 | /// end is O. F. 85 | pub const FRAMEOPTION_END_OFFSET_FOLLOWING: i32 = 0x04000; 86 | /// omit C.R. 87 | pub const FRAMEOPTION_EXCLUDE_CURRENT_ROW: i32 = 0x08000; 88 | /// omit C.R. & peers 89 | pub const FRAMEOPTION_EXCLUDE_GROUP: i32 = 0x10000; 90 | /// omit C.R.'s peers 91 | pub const FRAMEOPTION_EXCLUDE_TIES: i32 = 0x20000; 92 | 93 | pub const ATTRIBUTE_IDENTITY_ALWAYS: char = 'a'; 94 | pub const ATTRIBUTE_IDENTITY_BY_DEFAULT: char = 'd'; 95 | pub const ATTRIBUTE_GENERATED_STORED: char = 's'; 96 | 97 | pub const DEFAULT_INDEX_TYPE: &str = "btree"; 98 | 99 | pub const FKCONSTR_ACTION_NOACTION: char = 'a'; 100 | pub const FKCONSTR_ACTION_RESTRICT: char = 'r'; 101 | pub const FKCONSTR_ACTION_CASCADE: char = 'c'; 102 | pub const FKCONSTR_ACTION_SETNULL: char = 'n'; 103 | pub const FKCONSTR_ACTION_SETDEFAULT: char = 'd'; 104 | 105 | /* Foreign key matchtype codes */ 106 | pub const FKCONSTR_MATCH_FULL: char = 'f'; 107 | pub const FKCONSTR_MATCH_PARTIAL: char = 'p'; 108 | pub const FKCONSTR_MATCH_SIMPLE: char = 's'; 109 | 110 | /* Internal codes for partitioning strategies */ 111 | pub const PARTITION_STRATEGY_HASH: char = 'h'; 112 | pub const PARTITION_STRATEGY_LIST: char = 'l'; 113 | pub const PARTITION_STRATEGY_RANGE: char = 'r'; 114 | 115 | /* default selection for replica identity (primary key or nothing) */ 116 | pub const REPLICA_IDENTITY_DEFAULT: char = 'd'; 117 | /* no replica identity is logged for this relation */ 118 | pub const REPLICA_IDENTITY_NOTHING: char = 'n'; 119 | /* all columns are logged as replica identity */ 120 | pub const REPLICA_IDENTITY_FULL: char = 'f'; 121 | /* 122 | * an explicitly chosen candidate key's columns are used as replica identity. 123 | * Note this will still be set if the index has been dropped; in that case it 124 | * has the same meaning as 'd'. 125 | */ 126 | pub const REPLICA_IDENTITY_INDEX: char = 'i'; 127 | 128 | pub mod interval { 129 | pub const MONTH: i64 = 2; 130 | pub const YEAR: i64 = 4; 131 | pub const DAY: i64 = 8; 132 | pub const HOUR: i64 = 1024; 133 | pub const MINUTE: i64 = 2048; 134 | pub const SECOND: i64 = 4096; 135 | pub const YEAR_MONTH: i64 = YEAR | MONTH; 136 | pub const DAY_HOUR: i64 = DAY | HOUR; 137 | pub const DAY_HOUR_MINUTE: i64 = DAY | HOUR | MINUTE; 138 | pub const DAY_HOUR_MINUTE_SECOND: i64 = DAY | HOUR | MINUTE | SECOND; 139 | pub const HOUR_MINUTE: i64 = HOUR | MINUTE; 140 | pub const HOUR_MINUTE_SECOND: i64 = HOUR | MINUTE | SECOND; 141 | pub const MINUTE_SECOND: i64 = MINUTE | SECOND; 142 | pub const FULL_RANGE: i64 = 0x7FFF; 143 | pub const FULL_PRECISION: i64 = 0xFFFF; 144 | } 145 | 146 | pub mod lock { 147 | pub const AccessShareLock: i32 = 1; /* SELECT */ 148 | pub const RowShareLock: i32 = 2; /* SELECT FOR UPDATE/FOR SHARE */ 149 | pub const RowExclusiveLock: i32 = 3; /* INSERT, UPDATE, DELETE */ 150 | pub const ShareUpdateExclusiveLock: i32 = 4; /* VACUUM (non-FULL),ANALYZE, CREATE INDEX 151 | * CONCURRENTLY */ 152 | pub const ShareLock: i32 = 5; /* CREATE INDEX (WITHOUT CONCURRENTLY) */ 153 | pub const ShareRowExclusiveLock: i32 = 6; /* like EXCLUSIVE MODE, but allows ROW 154 | * SHARE */ 155 | pub const ExclusiveLock: i32 = 7; /* blocks ROW SHARE/SELECT...FOR UPDATE */ 156 | pub const AccessExclusiveLock: i32 = 8; /* ALTER TABLE, DROP TABLE, VACUUM FULL, 157 | * and unqualified LOCK TABLE */ 158 | } 159 | 160 | pub mod trigger { 161 | /* Bits within tgtype */ 162 | pub const TRIGGER_TYPE_AFTER: i16 = 0; 163 | pub const TRIGGER_TYPE_ROW: i16 = (1 << 0); 164 | pub const TRIGGER_TYPE_BEFORE: i16 = (1 << 1); 165 | pub const TRIGGER_TYPE_INSERT: i16 = (1 << 2); 166 | pub const TRIGGER_TYPE_DELETE: i16 = (1 << 3); 167 | pub const TRIGGER_TYPE_UPDATE: i16 = (1 << 4); 168 | pub const TRIGGER_TYPE_TRUNCATE: i16 = (1 << 5); 169 | pub const TRIGGER_TYPE_INSTEAD: i16 = (1 << 6); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/bindings.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | #![allow(deref_nullptr)] 5 | #![allow(unused)] 6 | #![allow(clippy::all)] 7 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 8 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | /// Error structure representing the basic error scenarios for `pg_parse`. 4 | #[derive(Debug, Clone, Eq, PartialEq)] 5 | pub enum Error { 6 | ParseError(String), 7 | InvalidAst(String), 8 | InvalidAstWithDebug(String, String), 9 | InvalidJson(String), 10 | } 11 | 12 | impl Display for Error { 13 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 14 | match self { 15 | Error::ParseError(value) => write!(f, "Parse Error: {}", value), 16 | Error::InvalidAst(value) => write!(f, "Invalid AST: {}", value), 17 | Error::InvalidAstWithDebug(value, debug) => { 18 | write!(f, "Invalid AST: {}. Debug: {}", value, debug) 19 | } 20 | Error::InvalidJson(value) => write!(f, "Invalid JSON: {}", value), 21 | } 22 | } 23 | } 24 | 25 | impl std::error::Error for Error {} 26 | 27 | /// Convenient Result alias for returning `pg_parse::Error`. 28 | pub type Result = core::result::Result; 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! pg_parse 2 | //! ============ 3 | //! 4 | //! PostgreSQL parser that uses the [actual PostgreSQL server source]((https://github.com/pganalyze/libpg_query)) to parse 5 | //! SQL queries and return the internal PostgreSQL parse tree. 6 | //! 7 | //! Warning! This library is in early stages of development so any APIs exposed are subject to change. 8 | //! 9 | //! ## Getting started 10 | //! 11 | //! Add the following to your `Cargo.toml` 12 | //! 13 | //! ```toml 14 | //! [dependencies] 15 | //! pg_parse = "0.9" 16 | //! ``` 17 | //! 18 | //! # Example: Parsing a query 19 | //! 20 | //! ```rust 21 | //! use pg_parse::ast::Node; 22 | //! 23 | //! let result = pg_parse::parse("SELECT * FROM contacts"); 24 | //! assert!(result.is_ok()); 25 | //! let result = result.unwrap(); 26 | //! assert!(matches!(*&result[0], Node::SelectStmt(_))); 27 | //! 28 | //! // We can also convert back to a string, if the `str` feature is enabled (enabled by default). 29 | //! #[cfg(feature = "str")] 30 | //! assert_eq!(result[0].to_string(), "SELECT * FROM contacts"); 31 | //! ``` 32 | //! 33 | 34 | /// Generated structures representing the PostgreSQL AST. 35 | pub mod ast; 36 | mod bindings; 37 | mod error; 38 | mod query; 39 | mod serde; 40 | #[cfg(feature = "str")] 41 | mod str; 42 | 43 | pub use error::*; 44 | pub use query::*; 45 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | use std::os::raw::c_char; 3 | 4 | use crate::bindings::*; 5 | use crate::error::*; 6 | 7 | #[derive(Debug, serde::Deserialize)] 8 | struct ParseResult { 9 | #[allow(unused)] 10 | version: u32, 11 | stmts: Vec, 12 | } 13 | 14 | #[derive(Debug, serde::Deserialize)] 15 | struct Stmt { 16 | stmt: crate::ast::Node, 17 | #[allow(unused)] 18 | stmt_len: Option, 19 | } 20 | 21 | /// Represents the resulting fingerprint containing both the raw integer form as well as the 22 | /// corresponding 16 character hex value. 23 | pub struct Fingerprint { 24 | pub value: u64, 25 | pub hex: String, 26 | } 27 | 28 | /// Parses the given SQL statement into the given abstract syntax tree. 29 | /// 30 | /// # Example 31 | /// 32 | /// ```rust 33 | /// use pg_parse::ast::Node; 34 | /// 35 | /// let result = pg_parse::parse("SELECT * FROM contacts"); 36 | /// assert!(result.is_ok()); 37 | /// let result = result.unwrap(); 38 | /// let el: &Node = &result[0]; 39 | /// assert!(matches!(*el, Node::SelectStmt(_))); 40 | /// ``` 41 | pub fn parse(stmt: &str) -> Result> { 42 | unsafe { 43 | let c_str = CString::new(stmt).unwrap(); 44 | let result = pg_query_parse(c_str.as_ptr() as *const c_char); 45 | 46 | // Capture any errors first 47 | if !result.error.is_null() { 48 | let error = &*result.error; 49 | let message = CStr::from_ptr(error.message).to_string_lossy().into(); 50 | pg_query_free_parse_result(result); 51 | return Err(Error::ParseError(message)); 52 | } 53 | 54 | // Parse the JSON into the AST 55 | let raw = CStr::from_ptr(result.parse_tree); 56 | let parsed: ParseResult = 57 | serde_json::from_slice(raw.to_bytes()).map_err(|e| Error::InvalidAst(e.to_string()))?; 58 | pg_query_free_parse_result(result); 59 | Ok(parsed.stmts.into_iter().map(|s| s.stmt).collect()) 60 | } 61 | } 62 | 63 | /// Similar to `parse`: parses the given SQL statement into the given abstract syntax tree 64 | /// but also returns the raw output generated by the postgres parser. 65 | /// 66 | /// # Example 67 | /// 68 | /// ```rust 69 | /// use pg_parse::ast::Node; 70 | /// 71 | /// let result = pg_parse::parse_debug("SELECT * FROM contacts"); 72 | /// assert!(result.is_ok()); 73 | /// let (stmt, raw) = result.unwrap(); 74 | /// let el: &Node = &stmt[0]; 75 | /// assert!(matches!(*el, Node::SelectStmt(_))); 76 | /// assert!(raw.contains("\"SelectStmt\"")); 77 | /// ``` 78 | pub fn parse_debug(stmt: &str) -> Result<(Vec, String)> { 79 | unsafe { 80 | let c_str = CString::new(stmt).unwrap(); 81 | let result = pg_query_parse(c_str.as_ptr() as *const c_char); 82 | 83 | // Capture any errors first 84 | if !result.error.is_null() { 85 | let error = &*result.error; 86 | let message = CStr::from_ptr(error.message).to_string_lossy().into(); 87 | pg_query_free_parse_result(result); 88 | return Err(Error::ParseError(message)); 89 | } 90 | 91 | // Parse the JSON into the AST 92 | let raw = CStr::from_ptr(result.parse_tree); 93 | let debug = raw.to_string_lossy().to_string(); 94 | let parsed: ParseResult = serde_json::from_slice(raw.to_bytes()) 95 | .map_err(|e| Error::InvalidAstWithDebug(e.to_string(), debug.to_string()))?; 96 | pg_query_free_parse_result(result); 97 | Ok((parsed.stmts.into_iter().map(|s| s.stmt).collect(), debug)) 98 | } 99 | } 100 | 101 | /// Normalizes the given SQL statement, returning a parameterized version. 102 | /// 103 | /// # Example 104 | /// 105 | /// ```rust 106 | /// let result = pg_parse::normalize("SELECT * FROM contacts WHERE name='Paul'"); 107 | /// assert!(result.is_ok()); 108 | /// let result = result.unwrap(); 109 | /// assert_eq!(result, "SELECT * FROM contacts WHERE name=$1"); 110 | /// ``` 111 | pub fn normalize(stmt: &str) -> Result { 112 | unsafe { 113 | let c_str = CString::new(stmt).unwrap(); 114 | let result = pg_query_normalize(c_str.as_ptr() as *const c_char); 115 | 116 | // Capture any errors first 117 | if !result.error.is_null() { 118 | let error = &*result.error; 119 | let message = CStr::from_ptr(error.message).to_string_lossy().into(); 120 | pg_query_free_normalize_result(result); 121 | return Err(Error::ParseError(message)); 122 | } 123 | 124 | // Parse the query back 125 | let raw = CStr::from_ptr(result.normalized_query); 126 | let owned = raw.to_string_lossy().to_string(); 127 | pg_query_free_normalize_result(result); 128 | Ok(owned) 129 | } 130 | } 131 | 132 | /// Fingerprints the given SQL statement. Useful for comparing parse trees across different implementations 133 | /// of `libpg_query`. 134 | /// 135 | /// # Example 136 | /// 137 | /// ```rust 138 | /// let result = pg_parse::fingerprint("SELECT * FROM contacts WHERE name='Paul'"); 139 | /// assert!(result.is_ok()); 140 | /// let result = result.unwrap(); 141 | /// assert_eq!(result.hex, "0e2581a461ece536"); 142 | /// ``` 143 | pub fn fingerprint(stmt: &str) -> Result { 144 | unsafe { 145 | let c_str = CString::new(stmt).unwrap(); 146 | let result = pg_query_fingerprint(c_str.as_ptr() as *const c_char); 147 | 148 | // Capture any errors first 149 | if !result.error.is_null() { 150 | let error = &*result.error; 151 | let message = CStr::from_ptr(error.message).to_string_lossy().into(); 152 | pg_query_free_fingerprint_result(result); 153 | return Err(Error::ParseError(message)); 154 | } 155 | 156 | // Parse the fingerprint 157 | let raw = CStr::from_ptr(result.fingerprint_str); 158 | let owned = Fingerprint { 159 | value: result.fingerprint, 160 | hex: raw.to_string_lossy().to_string(), 161 | }; 162 | pg_query_free_fingerprint_result(result); 163 | Ok(owned) 164 | } 165 | } 166 | 167 | /// An experimental API which parses a PLPGSQL function. This currently returns the raw JSON structure. 168 | /// 169 | /// # Example 170 | /// 171 | /// ```rust 172 | /// let result = pg_parse::parse_plpgsql( 173 | /// " \ 174 | /// CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name varchar, v_version varchar) \ 175 | /// RETURNS varchar AS $$ \ 176 | /// BEGIN \ 177 | /// IF v_version IS NULL THEN \ 178 | /// RETURN v_name; \ 179 | /// END IF; \ 180 | /// RETURN v_name || '/' || v_version; \ 181 | /// END; \ 182 | /// $$ LANGUAGE plpgsql;", 183 | /// ); 184 | /// assert!(result.is_ok()); 185 | /// ``` 186 | pub fn parse_plpgsql(stmt: &str) -> Result { 187 | unsafe { 188 | let c_str = CString::new(stmt).unwrap(); 189 | let result = pg_query_parse_plpgsql(c_str.as_ptr() as *const c_char); 190 | 191 | // Capture any errors first 192 | if !result.error.is_null() { 193 | let error = &*result.error; 194 | let message = CStr::from_ptr(error.message).to_string_lossy().into(); 195 | pg_query_free_plpgsql_parse_result(result); 196 | return Err(Error::ParseError(message)); 197 | } 198 | 199 | // Parse the pglpsql tree 200 | let raw = CStr::from_ptr(result.plpgsql_funcs); 201 | let owned = serde_json::from_str(&raw.to_string_lossy()) 202 | .map_err(|e| Error::InvalidJson(e.to_string()))?; 203 | pg_query_free_plpgsql_parse_result(result); 204 | Ok(owned) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/serde.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::ast::ConstValue; 4 | use serde::Deserialize; 5 | use serde::de::{Deserializer, Error, SeqAccess, Visitor}; 6 | 7 | pub(crate) fn deserialize_node_array<'de, D>( 8 | deserializer: D, 9 | ) -> Result, D::Error> 10 | where 11 | D: Deserializer<'de>, 12 | { 13 | #[derive(serde::Deserialize)] 14 | #[serde(untagged)] 15 | enum NodeOrError { 16 | Node(Box), 17 | // This consumes one "item" when `T` errors while deserializing. 18 | // This is necessary to make this work, when instead of having a direct value 19 | // like integer or string, the deserializer sees a list or map. 20 | Error(serde::de::IgnoredAny), 21 | } 22 | 23 | struct NodeArray; 24 | impl<'de> Visitor<'de> for NodeArray { 25 | type Value = Vec; 26 | 27 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 28 | formatter.write_str("Vec") 29 | } 30 | 31 | fn visit_seq(self, mut seq: A) -> Result 32 | where 33 | A: SeqAccess<'de>, 34 | { 35 | let mut values = Vec::with_capacity(seq.size_hint().unwrap_or_default()); 36 | 37 | while let Some(value) = seq.next_element()? { 38 | if let NodeOrError::Node(value) = value { 39 | values.push(*value); 40 | } 41 | } 42 | Ok(values) 43 | } 44 | } 45 | 46 | deserializer.deserialize_seq(NodeArray) 47 | } 48 | 49 | pub(crate) fn deserialize_node_array_opt<'de, D>( 50 | deserializer: D, 51 | ) -> Result>, D::Error> 52 | where 53 | D: Deserializer<'de>, 54 | { 55 | struct NodeArrayOpt; 56 | impl<'de> Visitor<'de> for NodeArrayOpt { 57 | type Value = Option>; 58 | 59 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 60 | formatter.write_str("Option>") 61 | } 62 | 63 | fn visit_none(self) -> Result 64 | where 65 | E: Error, 66 | { 67 | Ok(None) 68 | } 69 | 70 | fn visit_some(self, deserializer: D) -> Result 71 | where 72 | D: Deserializer<'de>, 73 | { 74 | let value = deserialize_node_array(deserializer)?; 75 | Ok(Some(value)) 76 | } 77 | } 78 | 79 | deserializer.deserialize_option(NodeArrayOpt) 80 | } 81 | 82 | pub(crate) fn deserialize_nested_string<'de, D>(deserializer: D) -> Result 83 | where 84 | D: Deserializer<'de>, 85 | { 86 | struct NestedString; 87 | impl<'de> Visitor<'de> for NestedString { 88 | type Value = String; 89 | 90 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 91 | formatter.write_str("String") 92 | } 93 | 94 | fn visit_map(self, mut map: V) -> Result 95 | where 96 | V: serde::de::MapAccess<'de>, 97 | { 98 | if let Some(key) = map.next_key::()? { 99 | if key.ne("sval") { 100 | return Err(Error::missing_field("sval")); 101 | } 102 | let value = map.next_value::()?; 103 | Ok(value) 104 | } else { 105 | Err(Error::missing_field("sval")) 106 | } 107 | } 108 | } 109 | 110 | deserializer.deserialize_map(NestedString) 111 | } 112 | 113 | pub(crate) fn deserialize_nested_string_opt<'de, D>( 114 | deserializer: D, 115 | ) -> Result, D::Error> 116 | where 117 | D: Deserializer<'de>, 118 | { 119 | struct NestedStringOpt; 120 | impl<'de> Visitor<'de> for NestedStringOpt { 121 | type Value = Option; 122 | 123 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 124 | formatter.write_str("Option") 125 | } 126 | 127 | fn visit_none(self) -> Result 128 | where 129 | E: Error, 130 | { 131 | Ok(None) 132 | } 133 | 134 | fn visit_some(self, deserializer: D) -> Result 135 | where 136 | D: Deserializer<'de>, 137 | { 138 | let value = deserialize_nested_string(deserializer)?; 139 | Ok(Some(value)) 140 | } 141 | } 142 | 143 | deserializer.deserialize_option(NestedStringOpt) 144 | } 145 | 146 | impl<'de> serde::Deserialize<'de> for ConstValue { 147 | fn deserialize(deserializer: D) -> Result 148 | where 149 | D: Deserializer<'de>, 150 | { 151 | struct ConstValueVisitor; 152 | 153 | impl<'de> Visitor<'de> for ConstValueVisitor { 154 | type Value = ConstValue; 155 | 156 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 157 | formatter.write_str("ConnectorTopics") 158 | } 159 | 160 | fn visit_map(self, mut map: V) -> Result 161 | where 162 | V: serde::de::MapAccess<'de>, 163 | { 164 | #[derive(Deserialize)] 165 | struct BoolValue { 166 | #[serde(default)] 167 | boolval: bool, 168 | } 169 | #[derive(Deserialize)] 170 | struct IntValue { 171 | #[serde(default)] 172 | ival: i64, 173 | } 174 | 175 | #[derive(Deserialize)] 176 | struct FloatValue { 177 | fval: String, 178 | } 179 | 180 | #[derive(Deserialize)] 181 | struct StringValue { 182 | sval: String, 183 | } 184 | 185 | #[derive(Deserialize)] 186 | struct BitStringValue { 187 | bsval: String, 188 | } 189 | 190 | fn maybe_location<'de, V>(mut inner: V) -> Result<(), V::Error> 191 | where 192 | V: serde::de::MapAccess<'de>, 193 | { 194 | // We may have a location after this which we need to consume 195 | if let Some(_location) = inner.next_key::()? { 196 | let _pos = inner.next_value::()?; 197 | } 198 | Ok(()) 199 | } 200 | 201 | if let Some(key) = map.next_key::()? { 202 | match key.as_str() { 203 | "boolval" => { 204 | let value = map.next_value::()?; 205 | maybe_location(map)?; 206 | Ok(ConstValue::Bool(value.boolval)) 207 | } 208 | "ival" => { 209 | let value = map.next_value::()?; 210 | maybe_location(map)?; 211 | Ok(ConstValue::Integer(value.ival)) 212 | } 213 | "fval" => { 214 | let value = map.next_value::()?; 215 | maybe_location(map)?; 216 | Ok(ConstValue::Float(value.fval)) 217 | } 218 | "sval" => { 219 | let value = map.next_value::()?; 220 | maybe_location(map)?; 221 | Ok(ConstValue::String(value.sval)) 222 | } 223 | "bsval" => { 224 | let value = map.next_value::()?; 225 | maybe_location(map)?; 226 | Ok(ConstValue::BitString(value.bsval)) 227 | } 228 | "isnull" => { 229 | let null = map.next_value::()?; 230 | maybe_location(map)?; 231 | if null { 232 | Ok(ConstValue::Null) 233 | } else { 234 | Ok(ConstValue::NotNull) 235 | } 236 | } 237 | unknown => Err(Error::unknown_field( 238 | unknown, 239 | &["boolval", "ival", "fval", "sval", "bsval", "isnull"], 240 | )), 241 | } 242 | } else { 243 | Err(Error::custom("expected value")) 244 | } 245 | } 246 | } 247 | 248 | deserializer.deserialize_map(ConstValueVisitor {}) 249 | } 250 | } 251 | 252 | #[cfg(test)] 253 | mod tests { 254 | use crate::ast::ConstValue; 255 | use crate::ast::Node; 256 | use serde::Deserialize; 257 | 258 | #[derive(Deserialize)] 259 | pub struct Nodes { 260 | #[serde(deserialize_with = "crate::serde::deserialize_node_array")] 261 | values: Vec, 262 | } 263 | 264 | #[derive(Deserialize)] 265 | pub struct OptionalNodes { 266 | #[serde(deserialize_with = "crate::serde::deserialize_node_array_opt", default)] 267 | values: Option>, 268 | } 269 | 270 | #[test] 271 | fn it_can_deserialize_a_node_array() { 272 | let json = "{ \"values\": [{ \"A_Const\": { \"ival\": { \"ival\": 10 }, \"location\": 253 } }, {}] }"; 273 | let nodes: Nodes = serde_json::from_str(json).unwrap(); 274 | assert_eq!(1, nodes.values.len()); 275 | assert!(matches!( 276 | nodes.values[0], 277 | Node::A_Const(ConstValue::Integer(10)) 278 | )) 279 | } 280 | 281 | #[test] 282 | fn it_can_deserialize_an_optional_node_array_with_missing_property() { 283 | let json = "{ }"; 284 | let nodes: OptionalNodes = serde_json::from_str(json).unwrap(); 285 | assert!(nodes.values.is_none()); 286 | } 287 | 288 | #[test] 289 | fn it_can_deserialize_an_optional_node_array_with_null() { 290 | let json = "{ \"values\": null }"; 291 | let nodes: OptionalNodes = serde_json::from_str(json).unwrap(); 292 | assert!(nodes.values.is_none()); 293 | } 294 | 295 | #[test] 296 | fn it_can_deserialize_an_optional_node_array_with_some() { 297 | let json = "{ \"values\": [{ \"Boolean\": { \"boolval\": false } }, {}] }"; 298 | let nodes: OptionalNodes = serde_json::from_str(json).unwrap(); 299 | assert!(nodes.values.is_some()); 300 | let values = nodes.values.unwrap(); 301 | assert_eq!(1, values.len()); 302 | assert!(matches!( 303 | values[0], 304 | Node::Boolean { 305 | boolval: Some(false) 306 | } 307 | )) 308 | } 309 | 310 | #[test] 311 | fn it_can_deserialize_an_optional_node_array_with_some_empty_array() { 312 | let json = "{\"values\":[{}]}"; 313 | let nodes: OptionalNodes = serde_json::from_str(json).unwrap(); 314 | assert!(nodes.values.is_some()); 315 | let values = nodes.values.unwrap(); 316 | assert_eq!(0, values.len()); 317 | } 318 | 319 | #[test] 320 | fn it_can_deserialize_const_with_location() { 321 | let tests = [ 322 | ( 323 | "{ \"ival\": { \"ival\": 10 }, \"location\": 253 }", 324 | ConstValue::Integer(10), 325 | ), 326 | ( 327 | "{ \"boolval\": { \"boolval\": true }, \"location\": 253 }", 328 | ConstValue::Bool(true), 329 | ), 330 | ( 331 | "{ \"fval\": { \"fval\": \"1.23\" }, \"location\": 253 }", 332 | ConstValue::Float("1.23".to_string()), 333 | ), 334 | ( 335 | "{ \"sval\": { \"sval\": \"hello\" }, \"location\": 253 }", 336 | ConstValue::String("hello".to_string()), 337 | ), 338 | ( 339 | "{ \"bsval\": { \"bsval\": \"b123\" }, \"location\": 253 }", 340 | ConstValue::BitString("b123".to_string()), 341 | ), 342 | ]; 343 | for (json, test) in &tests { 344 | let deserialized: ConstValue = 345 | serde_json::from_str(json).expect("Failed to deserialize"); 346 | assert_eq!(deserialized, *test, "Failed to deserialize: {}", json); 347 | } 348 | } 349 | 350 | #[test] 351 | fn it_can_deserialize_const_without_location() { 352 | let tests = [ 353 | ("{ \"ival\": { \"ival\": 10 } }", ConstValue::Integer(10)), 354 | ( 355 | "{ \"boolval\": { \"boolval\": true } }", 356 | ConstValue::Bool(true), 357 | ), 358 | ( 359 | "{ \"fval\": { \"fval\": \"1.23\" } }", 360 | ConstValue::Float("1.23".to_string()), 361 | ), 362 | ( 363 | "{ \"sval\": { \"sval\": \"hello\" } }", 364 | ConstValue::String("hello".to_string()), 365 | ), 366 | ( 367 | "{ \"bsval\": { \"bsval\": \"b123\" } }", 368 | ConstValue::BitString("b123".to_string()), 369 | ), 370 | ]; 371 | for (json, test) in &tests { 372 | let deserialized: ConstValue = 373 | serde_json::from_str(json).expect("Failed to deserialize"); 374 | assert_eq!(deserialized, *test, "Failed to deserialize: {}", json); 375 | } 376 | } 377 | 378 | #[test] 379 | fn it_can_deserialize_a_const() { 380 | // The structure of a_const changed dramatically which broke the deserialization. 381 | // Effectively, the definition of this is: 382 | // "A_Const": { 383 | // "fields": [ 384 | // { 385 | // "name": "isnull", 386 | // "c_type": "bool" 387 | // }, 388 | // { 389 | // "name": "val", 390 | // "c_type": "Node" 391 | // } 392 | // ] 393 | // } 394 | // The reality, however, is that nodes get sent in like: 395 | // // Works ok 396 | // "A_Const": 397 | // { 398 | // "isnull": true, 399 | // "location": 323 400 | // } 401 | // 402 | // // Does not work ok 403 | // "A_Const": 404 | // { 405 | // "ival": 406 | // { 407 | // "ival": 1 408 | // }, 409 | // "location": 123 410 | // } 411 | // Consequently, this test covers these cases 412 | let null_json = "{ \"A_Const\": { \"isnull\": true, \"location\": 323 } }"; 413 | let null_const: Node = serde_json::from_str(null_json).expect("Failed to deserialize"); 414 | let Node::A_Const(ConstValue::Null) = null_const else { 415 | panic!("Expected A_Const node: {:#?}", null_const); 416 | }; 417 | 418 | let ival_json = "{ \"A_Const\": { \"ival\": { \"ival\": 1 }, \"location\": 123 } }"; 419 | let ival_const: Node = serde_json::from_str(ival_json).expect("Failed to deserialize"); 420 | let Node::A_Const(ConstValue::Integer(val)) = ival_const else { 421 | panic!("Expected A_Const node: {:#?}", ival_const); 422 | }; 423 | assert_eq!(val, 1); 424 | } 425 | 426 | #[test] 427 | fn it_can_parse_an_empty_constant() { 428 | // This defaults to 0 - this is because it exits libpg_query like this, even for zero's. 429 | // We should keep an eye on this as 0 could be different than absence of data in the future. 430 | let json = "{ \"A_Const\": { \"ival\": {}, \"location\": 38 } }"; 431 | let node: Node = serde_json::from_str(json).unwrap(); 432 | assert!( 433 | matches!(node, Node::A_Const(ConstValue::Integer(0))), 434 | "Expected integer constant to default to 0" 435 | ); 436 | } 437 | 438 | #[test] 439 | fn it_can_parse_empty_nodes() { 440 | // This defaults to 0 - this is because it exits libpg_query like this, even for zero's. 441 | // We should keep an eye on this as 0 could be different than absence of data in the future. 442 | let json = "{\"Integer\":{}}"; 443 | let node: Node = serde_json::from_str(json).unwrap(); 444 | assert!( 445 | matches!(node, Node::Integer { ival: None }), 446 | "Expected integer node to default to None" 447 | ); 448 | } 449 | 450 | #[test] 451 | fn it_can_parse_directly_nested_strings() { 452 | #[derive(Deserialize)] 453 | struct Test { 454 | #[serde( 455 | deserialize_with = "crate::serde::deserialize_nested_string_opt", 456 | default 457 | )] 458 | extname: Option, 459 | } 460 | let json = "{\"extname\":{\"sval\":\"a\"}}"; 461 | let node: Test = serde_json::from_str(json).unwrap(); 462 | assert_eq!(node.extname, Some("a".to_string())); 463 | 464 | let json = "{}"; 465 | let node: Test = serde_json::from_str(json).unwrap(); 466 | assert_eq!(node.extname, None); 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/str.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod helpers; 3 | mod ext; 4 | mod nodes; 5 | 6 | use crate::ast::*; 7 | use ext::*; 8 | use std::fmt; 9 | 10 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 11 | enum Context { 12 | None, 13 | InsertRelation, 14 | AExpr, 15 | CreateType, 16 | AlterType, 17 | #[allow(dead_code)] 18 | Identifier, 19 | Constant, 20 | ForeignTable, 21 | } 22 | 23 | #[derive(Debug)] 24 | enum SqlError { 25 | Missing(String), 26 | UnexpectedConstValue(&'static str), 27 | UnexpectedNodeType(&'static str), 28 | UnexpectedObjectType(ObjectType), 29 | Unreachable, 30 | Unsupported(String), 31 | } 32 | 33 | impl fmt::Display for SqlError { 34 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 35 | match self { 36 | SqlError::Missing(field) => write!(f, "Missing field: {}", field), 37 | SqlError::UnexpectedConstValue(value) => write!(f, "Unexpected const value: {}", value), 38 | SqlError::UnexpectedNodeType(node) => write!(f, "Unexpected node type: {}", node), 39 | SqlError::UnexpectedObjectType(ty) => write!(f, "Unexpected object type: {:?}", ty), 40 | SqlError::Unreachable => write!(f, "Unreachable"), 41 | SqlError::Unsupported(message) => write!(f, "Unsupported feature: {}", message), 42 | } 43 | } 44 | } 45 | 46 | impl std::error::Error for SqlError {} 47 | 48 | trait SqlBuilder { 49 | fn build(&self, buffer: &mut String) -> Result<(), SqlError>; 50 | } 51 | 52 | trait SqlBuilderWithContext { 53 | fn build_with_context(&self, buffer: &mut String, context: Context) -> Result<(), SqlError>; 54 | } 55 | 56 | impl fmt::Display for Node { 57 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 58 | let mut buffer = String::new(); 59 | match self.build(&mut buffer) { 60 | Ok(_) => write!(f, "{}", buffer), 61 | Err(err) => { 62 | #[cfg(debug_assertions)] 63 | { 64 | eprintln!("Error generating SQL: {}", err); 65 | } 66 | Err(fmt::Error) 67 | } 68 | } 69 | } 70 | } 71 | 72 | impl fmt::Display for BoolExprType { 73 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 74 | match self { 75 | BoolExprType::AND_EXPR => write!(f, "AND"), 76 | BoolExprType::OR_EXPR => write!(f, "OR"), 77 | BoolExprType::NOT_EXPR => write!(f, "NOT"), 78 | } 79 | } 80 | } 81 | 82 | impl SqlBuilder for Node { 83 | fn build(&self, buffer: &mut String) -> Result<(), SqlError> { 84 | match self { 85 | Node::A_ArrayExpr(a_array_expr) => a_array_expr.build(buffer)?, 86 | Node::A_Const(constant) => constant.build(buffer)?, 87 | Node::A_Expr(a_expr) => a_expr.build_with_context(buffer, Context::None)?, 88 | Node::A_Indices(a_indices) => a_indices.build(buffer)?, 89 | Node::A_Indirection(a_indirection) => a_indirection.build(buffer)?, 90 | Node::A_Star(a_star) => a_star.build(buffer)?, 91 | Node::AccessPriv(privilege) => privilege.build(buffer)?, 92 | Node::AlterCollationStmt(stmt) => unsupported!(stmt), 93 | Node::AlterDatabaseSetStmt(stmt) => stmt.build(buffer)?, 94 | Node::AlterDatabaseStmt(stmt) => stmt.build(buffer)?, 95 | Node::AlterDefaultPrivilegesStmt(stmt) => unsupported!(stmt), 96 | Node::AlterDomainStmt(stmt) => unsupported!(stmt), 97 | Node::AlterEnumStmt(stmt) => unsupported!(stmt), 98 | Node::AlterEventTrigStmt(stmt) => unsupported!(stmt), 99 | Node::AlterExtensionContentsStmt(stmt) => stmt.build(buffer)?, 100 | Node::AlterExtensionStmt(stmt) => stmt.build(buffer)?, 101 | Node::AlterFdwStmt(stmt) => unsupported!(stmt), 102 | Node::AlterForeignServerStmt(stmt) => unsupported!(stmt), 103 | Node::AlterFunctionStmt(stmt) => unsupported!(stmt), 104 | Node::AlterObjectDependsStmt(stmt) => stmt.build(buffer)?, 105 | Node::AlterObjectSchemaStmt(stmt) => stmt.build(buffer)?, 106 | Node::AlterOpFamilyStmt(stmt) => unsupported!(stmt), 107 | Node::AlterOperatorStmt(stmt) => unsupported!(stmt), 108 | Node::AlterOwnerStmt(stmt) => unsupported!(stmt), 109 | Node::AlterPolicyStmt(stmt) => unsupported!(stmt), 110 | Node::AlterPublicationStmt(stmt) => unsupported!(stmt), 111 | Node::AlterRoleSetStmt(stmt) => unsupported!(stmt), 112 | Node::AlterRoleStmt(stmt) => unsupported!(stmt), 113 | Node::AlterSeqStmt(stmt) => unsupported!(stmt), 114 | Node::AlterStatsStmt(stmt) => unsupported!(stmt), 115 | Node::AlterSubscriptionStmt(stmt) => unsupported!(stmt), 116 | Node::AlterSystemStmt(stmt) => stmt.build(buffer)?, 117 | Node::AlterTSConfigurationStmt(stmt) => unsupported!(stmt), 118 | Node::AlterTSDictionaryStmt(stmt) => unsupported!(stmt), 119 | Node::AlterTableCmd(cmd) => cmd.build_with_context(buffer, Context::None)?, 120 | Node::AlterTableMoveAllStmt(stmt) => unsupported!(stmt), 121 | Node::AlterTableSpaceOptionsStmt(stmt) => stmt.build(buffer)?, 122 | Node::AlterTableStmt(stmt) => stmt.build(buffer)?, 123 | Node::AlterTypeStmt(stmt) => unsupported!(stmt), 124 | Node::AlterUserMappingStmt(stmt) => unsupported!(stmt), 125 | Node::CallContext(context) => unsupported!(context), 126 | Node::CallStmt(stmt) => unsupported!(stmt), 127 | Node::CheckPointStmt(stmt) => unsupported!(stmt), 128 | Node::ClosePortalStmt(stmt) => unsupported!(stmt), 129 | Node::ClusterStmt(stmt) => unsupported!(stmt), 130 | Node::CollateClause(collate) => collate.build(buffer)?, 131 | Node::ColumnDef(column) => column.build(buffer)?, 132 | Node::ColumnRef(column) => column.build(buffer)?, 133 | Node::CommentStmt(stmt) => stmt.build(buffer)?, 134 | Node::CommonTableExpr(cte) => cte.build(buffer)?, 135 | Node::CompositeTypeStmt(stmt) => stmt.build(buffer)?, 136 | Node::Constraint(constraint) => constraint.build(buffer)?, 137 | Node::ConstraintsSetStmt(stmt) => unsupported!(stmt), 138 | Node::CopyStmt(stmt) => stmt.build(buffer)?, 139 | Node::CreateAmStmt(stmt) => unsupported!(stmt), 140 | Node::CreateCastStmt(stmt) => stmt.build(buffer)?, 141 | Node::CreateConversionStmt(stmt) => unsupported!(stmt), 142 | Node::CreateDomainStmt(stmt) => stmt.build(buffer)?, 143 | Node::CreateEnumStmt(stmt) => stmt.build(buffer)?, 144 | Node::CreateEventTrigStmt(stmt) => unsupported!(stmt), 145 | Node::CreateExtensionStmt(stmt) => stmt.build(buffer)?, 146 | Node::CreateFdwStmt(stmt) => unsupported!(stmt), 147 | Node::CreateForeignServerStmt(stmt) => unsupported!(stmt), 148 | Node::CreateForeignTableStmt(stmt) => unsupported!(stmt), 149 | Node::CreateFunctionStmt(stmt) => stmt.build(buffer)?, 150 | Node::CreateOpClassItem(item) => unsupported!(item), 151 | Node::CreateOpClassStmt(stmt) => unsupported!(stmt), 152 | Node::CreateOpFamilyStmt(stmt) => unsupported!(stmt), 153 | Node::CreatePLangStmt(stmt) => unsupported!(stmt), 154 | Node::CreatePolicyStmt(stmt) => unsupported!(stmt), 155 | Node::CreatePublicationStmt(stmt) => unsupported!(stmt), 156 | Node::CreateRangeStmt(stmt) => stmt.build(buffer)?, 157 | Node::CreateRoleStmt(stmt) => unsupported!(stmt), 158 | Node::CreateSchemaStmt(stmt) => stmt.build(buffer)?, 159 | Node::CreateSeqStmt(stmt) => stmt.build(buffer)?, 160 | Node::CreateStatsStmt(stmt) => unsupported!(stmt), 161 | Node::CreateStmt(stmt) => stmt.build_with_context(buffer, Context::None)?, 162 | Node::CreateSubscriptionStmt(stmt) => unsupported!(stmt), 163 | Node::CreateTableAsStmt(stmt) => stmt.build(buffer)?, 164 | Node::CreateTableSpaceStmt(stmt) => stmt.build(buffer)?, 165 | Node::CreateTransformStmt(stmt) => unsupported!(stmt), 166 | Node::CreateTrigStmt(stmt) => stmt.build(buffer)?, 167 | Node::CreateUserMappingStmt(stmt) => unsupported!(stmt), 168 | Node::CreatedbStmt(stmt) => stmt.build(buffer)?, 169 | Node::DeallocateStmt(stmt) => unsupported!(stmt), 170 | Node::DeclareCursorStmt(stmt) => unsupported!(stmt), 171 | Node::DefElem(elem) => unsupported!(elem), 172 | Node::DefineStmt(stmt) => stmt.build(buffer)?, 173 | Node::DeleteStmt(stmt) => stmt.build(buffer)?, 174 | Node::DiscardStmt(stmt) => stmt.build(buffer)?, 175 | Node::DoStmt(stmt) => stmt.build(buffer)?, 176 | Node::DropOwnedStmt(stmt) => unsupported!(stmt), 177 | Node::DropRoleStmt(stmt) => stmt.build(buffer)?, 178 | Node::DropStmt(stmt) => stmt.build(buffer)?, 179 | Node::DropSubscriptionStmt(stmt) => stmt.build(buffer)?, 180 | Node::DropTableSpaceStmt(stmt) => stmt.build(buffer)?, 181 | Node::DropUserMappingStmt(stmt) => unsupported!(stmt), 182 | Node::DropdbStmt(stmt) => unsupported!(stmt), 183 | Node::ExecuteStmt(stmt) => stmt.build(buffer)?, 184 | Node::ExplainStmt(stmt) => stmt.build(buffer)?, 185 | Node::FetchStmt(stmt) => unsupported!(stmt), 186 | Node::FuncCall(func) => func.build(buffer)?, 187 | Node::FunctionParameter(parameter) => parameter.build(buffer)?, 188 | Node::GrantRoleStmt(stmt) => stmt.build(buffer)?, 189 | Node::GrantStmt(stmt) => stmt.build(buffer)?, 190 | Node::GroupingSet(set) => set.build(buffer)?, 191 | Node::ImportForeignSchemaStmt(stmt) => unsupported!(stmt), 192 | Node::IndexElem(elem) => elem.build(buffer)?, 193 | Node::IndexStmt(stmt) => stmt.build(buffer)?, 194 | Node::InferClause(clause) => clause.build(buffer)?, 195 | Node::InlineCodeBlock(block) => unsupported!(block), 196 | Node::InsertStmt(stmt) => stmt.build(buffer)?, 197 | Node::ListenStmt(stmt) => unsupported!(stmt), 198 | Node::LoadStmt(stmt) => stmt.build(buffer)?, 199 | Node::LockStmt(stmt) => stmt.build(buffer)?, 200 | Node::LockingClause(clause) => clause.build(buffer)?, 201 | Node::MultiAssignRef(stmt) => unsupported!(stmt), 202 | Node::NotifyStmt(stmt) => unsupported!(stmt), 203 | Node::ObjectWithArgs(args) => unsupported!(args), 204 | Node::OnConflictClause(clause) => clause.build(buffer)?, 205 | Node::ParamRef(param) => param.build(buffer)?, 206 | Node::PartitionBoundSpec(bound) => bound.build(buffer)?, 207 | Node::PartitionCmd(cmd) => cmd.build(buffer)?, 208 | Node::PartitionElem(elem) => elem.build(buffer)?, 209 | Node::PartitionRangeDatum(datum) => unsupported!(datum), 210 | Node::PartitionSpec(spec) => spec.build(buffer)?, 211 | Node::PrepareStmt(stmt) => stmt.build(buffer)?, 212 | Node::Query(query) => unsupported!(query), 213 | Node::RangeFunction(func) => func.build(buffer)?, 214 | Node::RangeSubselect(select) => select.build(buffer)?, 215 | Node::RangeTableFunc(func) => func.build(buffer)?, 216 | Node::RangeTableFuncCol(col) => col.build(buffer)?, 217 | Node::RangeTableSample(sample) => sample.build(buffer)?, 218 | Node::RangeTblEntry(entry) => unsupported!(entry), 219 | Node::RangeTblFunction(function) => unsupported!(function), 220 | Node::RawStmt(stmt) => unsupported!(stmt), 221 | Node::ReassignOwnedStmt(stmt) => unsupported!(stmt), 222 | Node::RefreshMatViewStmt(stmt) => unsupported!(stmt), 223 | Node::ReindexStmt(stmt) => unsupported!(stmt), 224 | Node::RenameStmt(stmt) => stmt.build(buffer)?, 225 | Node::ReplicaIdentityStmt(stmt) => stmt.build(buffer)?, 226 | Node::ResTarget(target) => target.build(buffer)?, 227 | Node::RoleSpec(role) => role.build(buffer)?, 228 | Node::RowMarkClause(clause) => unsupported!(clause), 229 | Node::RuleStmt(stmt) => unsupported!(stmt), 230 | Node::SecLabelStmt(stmt) => unsupported!(stmt), 231 | Node::SelectStmt(stmt) => stmt.build(buffer)?, 232 | Node::SetOperationStmt(stmt) => unsupported!(stmt), 233 | Node::SortBy(sort) => sort.build(buffer)?, 234 | Node::SortGroupClause(clause) => unsupported!(clause), 235 | Node::TableLikeClause(clause) => clause.build(buffer)?, 236 | Node::TableSampleClause(clause) => unsupported!(clause), 237 | Node::TransactionStmt(stmt) => stmt.build(buffer)?, 238 | Node::TriggerTransition(transition) => transition.build(buffer)?, 239 | Node::TruncateStmt(stmt) => unsupported!(stmt), 240 | Node::TypeCast(cast) => cast.build(buffer)?, 241 | Node::TypeName(name) => name.build(buffer)?, 242 | Node::UnlistenStmt(stmt) => unsupported!(stmt), 243 | Node::UpdateStmt(stmt) => stmt.build(buffer)?, 244 | Node::VacuumRelation(relation) => unsupported!(relation), 245 | Node::VacuumStmt(stmt) => stmt.build(buffer)?, 246 | Node::VariableSetStmt(stmt) => stmt.build(buffer)?, 247 | Node::VariableShowStmt(stmt) => unsupported!(stmt), 248 | Node::ViewStmt(stmt) => stmt.build(buffer)?, 249 | Node::WindowClause(clause) => unsupported!(clause), 250 | Node::WindowDef(def) => def.build(buffer)?, 251 | Node::WithCheckOption(option) => unsupported!(option), 252 | Node::WithClause(with) => with.build(buffer)?, 253 | Node::XmlSerialize(xml) => xml.build(buffer)?, 254 | Node::Aggref(stmt) => unsupported!(stmt), 255 | Node::Alias(alias) => alias.build(buffer)?, 256 | Node::AlternativeSubPlan(plan) => unsupported!(plan), 257 | Node::ArrayCoerceExpr(expr) => unsupported!(expr), 258 | Node::ArrayExpr(expr) => unsupported!(expr), 259 | Node::BoolExpr(expr) => expr.build(buffer)?, 260 | Node::BooleanTest(test) => test.build(buffer)?, 261 | Node::CaseExpr(expr) => expr.build(buffer)?, 262 | Node::CaseTestExpr(expr) => unsupported!(expr), 263 | Node::CaseWhen(when) => when.build(buffer)?, 264 | Node::CoalesceExpr(expr) => expr.build(buffer)?, 265 | Node::CoerceToDomain(stmt) => unsupported!(stmt), 266 | Node::CoerceToDomainValue(stmt) => unsupported!(stmt), 267 | Node::CoerceViaIO(stmt) => unsupported!(stmt), 268 | Node::CollateExpr(expr) => unsupported!(expr), 269 | Node::Const(stmt) => unsupported!(stmt), 270 | Node::ConvertRowtypeExpr(expr) => unsupported!(expr), 271 | Node::CurrentOfExpr(expr) => expr.build(buffer)?, 272 | Node::FieldSelect(stmt) => unsupported!(stmt), 273 | Node::FieldStore(stmt) => unsupported!(stmt), 274 | Node::FromExpr(expr) => unsupported!(expr), 275 | Node::FuncExpr(expr) => unsupported!(expr), 276 | Node::GroupingFunc(stmt) => stmt.build(buffer)?, 277 | Node::InferenceElem(stmt) => unsupported!(stmt), 278 | Node::IntoClause(into) => into.build(buffer)?, 279 | Node::JoinExpr(expr) => expr.build(buffer)?, 280 | Node::MinMaxExpr(expr) => expr.build(buffer)?, 281 | Node::NamedArgExpr(expr) => unsupported!(expr), 282 | Node::NextValueExpr(expr) => unsupported!(expr), 283 | Node::NullTest(test) => test.build(buffer)?, 284 | Node::OnConflictExpr(expr) => unsupported!(expr), 285 | Node::OpExpr(expr) => unsupported!(expr), 286 | Node::Param(param) => unsupported!(param), 287 | Node::RangeTblRef(expr) => unsupported!(expr), 288 | Node::RangeVar(range) => range.build_with_context(buffer, Context::None)?, 289 | Node::RelabelType(expr) => unsupported!(expr), 290 | Node::RowCompareExpr(expr) => unsupported!(expr), 291 | Node::RowExpr(expr) => expr.build(buffer)?, 292 | Node::SQLValueFunction(func) => func.build(buffer)?, 293 | Node::ScalarArrayOpExpr(expr) => unsupported!(expr), 294 | Node::SetToDefault(set) => set.build(buffer)?, 295 | Node::SubLink(link) => link.build(buffer)?, 296 | Node::SubPlan(plan) => unsupported!(plan), 297 | Node::SubscriptingRef(expr) => unsupported!(expr), 298 | Node::TableFunc(expr) => unsupported!(expr), 299 | Node::TargetEntry(expr) => unsupported!(expr), 300 | Node::Var(expr) => unsupported!(expr), 301 | Node::WindowFunc(expr) => unsupported!(expr), 302 | Node::XmlExpr(expr) => expr.build(buffer)?, 303 | Node::List(list) => { 304 | for (index, item) in list.items.iter().enumerate() { 305 | if index > 0 { 306 | buffer.push_str(", "); 307 | } 308 | item.build(buffer)?; 309 | } 310 | } 311 | Node::BitString { .. } 312 | | Node::Boolean { .. } 313 | | Node::Float { .. } 314 | | Node::Integer { .. } 315 | | Node::String { .. } => SqlValue(self).build_with_context(buffer, Context::None)?, 316 | 317 | Node::AlterDatabaseRefreshCollStmt(stmt) => unsupported!(stmt), 318 | Node::CTECycleClause(clause) => unsupported!(clause), 319 | Node::CTESearchClause(clause) => unsupported!(clause), 320 | Node::MergeAction(action) => unsupported!(action), 321 | Node::MergeStmt(stmt) => unsupported!(stmt), 322 | Node::MergeWhenClause(clause) => unsupported!(clause), 323 | Node::PLAssignStmt(stmt) => unsupported!(stmt), 324 | Node::PublicationObjSpec(spec) => unsupported!(spec), 325 | Node::PublicationTable(table) => unsupported!(table), 326 | Node::ReturnStmt(stmt) => unsupported!(stmt), 327 | Node::StatsElem(elem) => unsupported!(elem), 328 | 329 | Node::JsonAggConstructor(arg) => unsupported!(arg), 330 | Node::JsonArgument(arg) => unsupported!(arg), 331 | Node::JsonArrayAgg(arg) => unsupported!(arg), 332 | Node::JsonArrayConstructor(arg) => unsupported!(arg), 333 | Node::JsonArrayQueryConstructor(arg) => unsupported!(arg), 334 | Node::JsonFuncExpr(arg) => unsupported!(arg), 335 | Node::JsonKeyValue(arg) => unsupported!(arg), 336 | Node::JsonObjectAgg(arg) => unsupported!(arg), 337 | Node::JsonObjectConstructor(arg) => unsupported!(arg), 338 | Node::JsonOutput(arg) => unsupported!(arg), 339 | Node::JsonParseExpr(arg) => unsupported!(arg), 340 | Node::JsonScalarExpr(arg) => unsupported!(arg), 341 | Node::JsonSerializeExpr(arg) => unsupported!(arg), 342 | Node::JsonTable(arg) => unsupported!(arg), 343 | Node::JsonTableColumn(arg) => unsupported!(arg), 344 | Node::JsonTablePathSpec(arg) => unsupported!(arg), 345 | Node::RTEPermissionInfo(arg) => unsupported!(arg), 346 | Node::SinglePartitionSpec(arg) => unsupported!(arg), 347 | Node::JsonBehavior(arg) => unsupported!(arg), 348 | Node::JsonConstructorExpr(arg) => unsupported!(arg), 349 | Node::JsonExpr(arg) => unsupported!(arg), 350 | Node::JsonFormat(arg) => unsupported!(arg), 351 | Node::JsonIsPredicate(arg) => unsupported!(arg), 352 | Node::JsonReturning(arg) => unsupported!(arg), 353 | Node::JsonTablePath(arg) => unsupported!(arg), 354 | Node::JsonTablePathScan(arg) => unsupported!(arg), 355 | Node::JsonTablePlan(arg) => unsupported!(arg), 356 | Node::JsonTableSiblingJoin(arg) => unsupported!(arg), 357 | Node::JsonValueExpr(arg) => unsupported!(arg), 358 | Node::MergeSupportFunc(arg) => unsupported!(arg), 359 | Node::WindowFuncRunCondition(arg) => unsupported!(arg), 360 | } 361 | Ok(()) 362 | } 363 | } 364 | 365 | #[cfg(test)] 366 | mod tests { 367 | use crate::ast::{A_Star, Node}; 368 | 369 | #[test] 370 | fn it_can_convert_a_struct_node_to_string() { 371 | let node = Node::A_Star(A_Star {}); 372 | assert_eq!("A_Star", node.name()); 373 | } 374 | 375 | #[test] 376 | fn it_can_convert_a_value_node_to_string() { 377 | let node = Node::Integer { ival: Some(5) }; 378 | assert_eq!("Integer", node.name()); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/str/helpers.rs: -------------------------------------------------------------------------------- 1 | use crate::ast::Node; 2 | use crate::str::SqlError; 3 | 4 | macro_rules! must { 5 | ($expr:expr) => { 6 | $expr 7 | .as_ref() 8 | .ok_or_else(|| SqlError::Missing(stringify!($expr).into()))? 9 | }; 10 | } 11 | 12 | macro_rules! node { 13 | ($expr:expr, $ty:path) => { 14 | match &$expr { 15 | $ty(elem) => elem, 16 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 17 | } 18 | }; 19 | } 20 | 21 | macro_rules! const_integer { 22 | ($expr:expr) => { 23 | match &$expr { 24 | Node::A_Const(value) => match &value { 25 | crate::ast::ConstValue::Integer(value) => value, 26 | unexpected => return Err(SqlError::UnexpectedConstValue(unexpected.name())), 27 | }, 28 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 29 | } 30 | }; 31 | } 32 | 33 | macro_rules! const_string { 34 | ($expr:expr) => { 35 | match &$expr { 36 | Node::A_Const(value) => match &value { 37 | crate::ast::ConstValue::String(value) => value, 38 | unexpected => return Err(SqlError::UnexpectedConstValue(unexpected.name())), 39 | }, 40 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 41 | } 42 | }; 43 | } 44 | 45 | macro_rules! iter_only { 46 | ($expr:expr, $ty:path) => { 47 | $expr.iter().filter_map(|n| match n { 48 | $ty(elem) => Some(elem), 49 | _ => None, 50 | }) 51 | }; 52 | } 53 | macro_rules! bool_value { 54 | ($expr:expr) => { 55 | match &$expr { 56 | Node::Boolean { boolval: value } => value, 57 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 58 | } 59 | }; 60 | } 61 | 62 | macro_rules! int_value { 63 | ($expr:expr) => { 64 | match &$expr { 65 | Node::Integer { ival: value } => value, 66 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 67 | } 68 | }; 69 | } 70 | 71 | macro_rules! string_value { 72 | ($expr:expr) => { 73 | match &$expr { 74 | Node::String { sval: Some(value) } => value, 75 | unexpected => return Err(SqlError::UnexpectedNodeType(unexpected.name())), 76 | } 77 | }; 78 | } 79 | 80 | macro_rules! unsupported { 81 | ($expr:expr) => { 82 | return Err(SqlError::Unsupported(format!("{:?}", $expr))) 83 | }; 84 | } 85 | 86 | pub(in crate::str) fn join_strings( 87 | buffer: &mut String, 88 | nodes: &[Node], 89 | delim: &str, 90 | ) -> Result<(), SqlError> { 91 | for (index, node) in nodes.iter().enumerate() { 92 | if index > 0 { 93 | buffer.push_str(delim); 94 | } 95 | if let Node::String { sval: Some(value) } = node { 96 | buffer.push_str("e_identifier(value)); 97 | } else { 98 | return Err(SqlError::UnexpectedNodeType(node.name())); 99 | } 100 | } 101 | Ok(()) 102 | } 103 | 104 | pub(in crate::str) fn quote_identifier(ident: &str) -> String { 105 | if ident.is_empty() { 106 | return String::new(); 107 | } 108 | 109 | // future: Use the direct PostgreSQL function 110 | // For now, we partially reproduce it 111 | // We don't need to quote if the identifier starts with a lowercase letter or underscore 112 | // and contains only lowercase letters, digits and underscores, AND is not a reserved keyword. 113 | let chars = ident.chars().collect::>(); 114 | let safe = chars 115 | .iter() 116 | .all(|c| c.is_lowercase() || c.is_ascii_digit() || *c == '_') 117 | && !chars[0].is_ascii_digit(); 118 | if safe && !is_keyword(ident) { 119 | ident.to_string() 120 | } else { 121 | format!("\"{}\"", ident) 122 | } 123 | } 124 | 125 | pub(in crate::str) fn persistence_from_code(code: char) -> Option<&'static str> { 126 | match code { 127 | // Regular table 128 | 'p' => None, 129 | 'u' => Some("UNLOGGED"), 130 | 't' => Some("TEMPORARY"), 131 | // Just ignore rather than error 132 | _ => None, 133 | } 134 | } 135 | 136 | pub(in crate::str) fn node_vec_to_string_vec(nodes: &[Node]) -> Vec<&String> { 137 | nodes 138 | .iter() 139 | .filter_map(|n| match n { 140 | Node::String { sval: Some(value) } => Some(value), 141 | _ => None, 142 | }) 143 | .collect::>() 144 | } 145 | 146 | // Returns true if the operator contains ONLY operator characters 147 | pub(in crate::str) fn is_operator(op: &str) -> bool { 148 | for char in op.chars() { 149 | match char { 150 | '~' | '!' | '@' | '#' | '^' | '&' | '|' | '`' | '?' | '+' | '-' | '*' | '/' | '%' 151 | | '<' | '>' | '=' => {} 152 | _ => return false, 153 | } 154 | } 155 | true 156 | } 157 | 158 | pub(in crate::str) fn is_keyword(ident: &str) -> bool { 159 | matches!( 160 | &ident.to_ascii_lowercase()[..], 161 | "all" 162 | | "analyse" 163 | | "analyze" 164 | | "and" 165 | | "any" 166 | | "array" 167 | | "as" 168 | | "asc" 169 | | "asymmetric" 170 | | "authorization" 171 | | "binary" 172 | | "both" 173 | | "case" 174 | | "cast" 175 | | "check" 176 | | "collate" 177 | | "collation" 178 | | "column" 179 | | "concurrently" 180 | | "constraint" 181 | | "create" 182 | | "cross" 183 | | "current_catalog" 184 | | "current_date" 185 | | "current_role" 186 | | "current_schema" 187 | | "current_time" 188 | | "current_timestamp" 189 | | "current_user" 190 | | "default" 191 | | "deferrable" 192 | | "desc" 193 | | "distinct" 194 | | "do" 195 | | "else" 196 | | "end" 197 | | "except" 198 | | "false" 199 | | "fetch" 200 | | "for" 201 | | "foreign" 202 | | "freeze" 203 | | "from" 204 | | "full" 205 | | "grant" 206 | | "group" 207 | | "having" 208 | | "ilike" 209 | | "in" 210 | | "initially" 211 | | "inner" 212 | | "intersect" 213 | | "into" 214 | | "is" 215 | | "isnull" 216 | | "join" 217 | | "lateral" 218 | | "leading" 219 | | "left" 220 | | "like" 221 | | "limit" 222 | | "localtime" 223 | | "localtimestamp" 224 | | "natural" 225 | | "not" 226 | | "notnull" 227 | | "null" 228 | | "offset" 229 | | "on" 230 | | "only" 231 | | "or" 232 | | "order" 233 | | "outer" 234 | | "overlaps" 235 | | "placing" 236 | | "primary" 237 | | "references" 238 | | "returning" 239 | | "right" 240 | | "select" 241 | | "session_user" 242 | | "similar" 243 | | "some" 244 | | "symmetric" 245 | | "table" 246 | | "tablesample" 247 | | "then" 248 | | "to" 249 | | "trailing" 250 | | "true" 251 | | "union" 252 | | "unique" 253 | | "user" 254 | | "using" 255 | | "variadic" 256 | | "verbose" 257 | | "when" 258 | | "where" 259 | | "window" 260 | | "with" 261 | ) 262 | } 263 | 264 | pub(in crate::str) fn non_reserved_word_or_sconst( 265 | buffer: &mut String, 266 | val: &str, 267 | ) -> Result<(), SqlError> { 268 | if val.is_empty() { 269 | buffer.push_str("''"); 270 | } else if val.len() >= 64 { 271 | // NAMEDATALEN constant in pg 272 | use crate::str::SqlBuilder; 273 | crate::str::ext::StringLiteral(val).build(buffer)?; 274 | } else { 275 | buffer.push_str("e_identifier(val)); 276 | } 277 | Ok(()) 278 | } 279 | -------------------------------------------------------------------------------- /tests/data/sql/func_1.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION geo.fn_do_any_coordinates_fall_inside(geom geometry(MultiPolygonZM), coordinates text[][]) 2 | RETURNS boolean AS $$ 3 | SELECT 4 | -- Haha - a bit shit! Obvious need for immediate attention 5 | CASE array_length(coordinates, 1) 6 | WHEN 1 THEN 7 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) 8 | WHEN 2 THEN 9 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 10 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) 11 | WHEN 3 THEN 12 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 13 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 14 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) 15 | WHEN 4 THEN 16 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 17 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 18 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 19 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) 20 | WHEN 5 THEN 21 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 22 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 23 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 24 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 25 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) 26 | WHEN 6 THEN 27 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 28 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 29 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 30 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 31 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) OR 32 | ST_CONTAINS(geom, ST_POINT(coordinates[6][2]::double precision, coordinates[6][1]::double precision)) 33 | WHEN 7 THEN 34 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 35 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 36 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 37 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 38 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) OR 39 | ST_CONTAINS(geom, ST_POINT(coordinates[6][2]::double precision, coordinates[6][1]::double precision)) OR 40 | ST_CONTAINS(geom, ST_POINT(coordinates[7][2]::double precision, coordinates[7][1]::double precision)) 41 | WHEN 8 THEN 42 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 43 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 44 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 45 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 46 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) OR 47 | ST_CONTAINS(geom, ST_POINT(coordinates[6][2]::double precision, coordinates[6][1]::double precision)) OR 48 | ST_CONTAINS(geom, ST_POINT(coordinates[7][2]::double precision, coordinates[7][1]::double precision)) OR 49 | ST_CONTAINS(geom, ST_POINT(coordinates[8][2]::double precision, coordinates[8][1]::double precision)) 50 | WHEN 9 THEN 51 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 52 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 53 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 54 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 55 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) OR 56 | ST_CONTAINS(geom, ST_POINT(coordinates[6][2]::double precision, coordinates[6][1]::double precision)) OR 57 | ST_CONTAINS(geom, ST_POINT(coordinates[7][2]::double precision, coordinates[7][1]::double precision)) OR 58 | ST_CONTAINS(geom, ST_POINT(coordinates[8][2]::double precision, coordinates[8][1]::double precision)) OR 59 | ST_CONTAINS(geom, ST_POINT(coordinates[9][2]::double precision, coordinates[9][1]::double precision)) 60 | WHEN 10 THEN 61 | ST_CONTAINS(geom, ST_POINT(coordinates[1][2]::double precision, coordinates[1][1]::double precision)) OR 62 | ST_CONTAINS(geom, ST_POINT(coordinates[2][2]::double precision, coordinates[2][1]::double precision)) OR 63 | ST_CONTAINS(geom, ST_POINT(coordinates[3][2]::double precision, coordinates[3][1]::double precision)) OR 64 | ST_CONTAINS(geom, ST_POINT(coordinates[4][2]::double precision, coordinates[4][1]::double precision)) OR 65 | ST_CONTAINS(geom, ST_POINT(coordinates[5][2]::double precision, coordinates[5][1]::double precision)) OR 66 | ST_CONTAINS(geom, ST_POINT(coordinates[6][2]::double precision, coordinates[6][1]::double precision)) OR 67 | ST_CONTAINS(geom, ST_POINT(coordinates[7][2]::double precision, coordinates[7][1]::double precision)) OR 68 | ST_CONTAINS(geom, ST_POINT(coordinates[8][2]::double precision, coordinates[8][1]::double precision)) OR 69 | ST_CONTAINS(geom, ST_POINT(coordinates[9][2]::double precision, coordinates[9][1]::double precision)) OR 70 | ST_CONTAINS(geom, ST_POINT(coordinates[10][2]::double precision, coordinates[10][1]::double precision)) 71 | ELSE 72 | FALSE 73 | END 74 | $$ 75 | LANGUAGE SQL; -------------------------------------------------------------------------------- /tests/data/sql/func_2.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION collab.sp_new_tax(country character varying(2), state character varying(10), definition json) 2 | RETURNS INT AS $$ 3 | DECLARE 4 | cid int; 5 | sid int; 6 | new_id int; 7 | BEGIN 8 | 9 | SELECT INTO cid id FROM reference_data.countries WHERE iso=country; 10 | IF NOT FOUND THEN 11 | RAISE EXCEPTION 'country % not found', country; 12 | END IF; 13 | 14 | IF state = 'FED' THEN 15 | sid = NULL; 16 | ELSE 17 | SELECT INTO sid id FROM reference_data.states WHERE iso=state AND country_id=cid; 18 | IF NOT FOUND THEN 19 | RAISE EXCEPTION 'state % not found', state; 20 | END IF; 21 | END IF; 22 | 23 | INSERT INTO collab.tax_definitions(country_id, state_id, status_id, date_created, date_modified, definition) 24 | VALUES(cid, sid, 1, now(), now(), definition) RETURNING id INTO new_id; 25 | 26 | RETURN new_id; 27 | END; 28 | $$ LANGUAGE plpgsql; -------------------------------------------------------------------------------- /tests/data/sql/table_1.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE geo.states 2 | ( 3 | gid serial NOT NULL, 4 | statefp character varying(2), 5 | statens character varying(8), 6 | affgeoid character varying(11), 7 | geoid character varying(2), 8 | stusps character varying(2), 9 | name character varying(100), 10 | lsad character varying(2), 11 | aland double precision, 12 | awater double precision, 13 | geom geometry(MultiPolygonZM), 14 | CONSTRAINT pk_geo_states PRIMARY KEY (gid) 15 | ); -------------------------------------------------------------------------------- /tests/data/sql/view_1.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW account_transaction_with_balance AS 2 | SELECT t.id, t.customer_id, t.account_id, t.transaction_date, t.fit_id, t.description, t.debit, t.credit, t.source, t.category, 3 | (COALESCE(sum_debit, 0::numeric) - COALESCE(sum_credit, 0::numeric)) + a.opening_balance "balance", 4 | r.reconciled_association_type, 5 | r.reconciled_association_id, 6 | r.reconciled_association_name, 7 | r.reconciled_transaction_desc 8 | FROM 9 | ( 10 | SELECT at.*, 11 | SUM(debit) OVER (PARTITION BY account_id ORDER BY transaction_date, id) sum_debit, 12 | SUM(credit) OVER (PARTITION BY account_id ORDER BY transaction_date, id) sum_credit 13 | FROM account_transaction at 14 | ) t 15 | INNER JOIN account a ON a.id = t.account_id AND a.customer_id = t.customer_id 16 | LEFT JOIN 17 | ( 18 | SELECT r.customer_id, r.source_transaction_id, 19 | -- Since 1 account transaction = multiple envelope in plan, we just use the plan 20 | CASE WHEN rs.id IS NOT NULL THEN 'plan' WHEN ra.id IS NOT NULL THEN 'account' WHEN re.id IS NOT NULL THEN 'envelope' END::text "reconciled_association_type", 21 | COALESCE(rs.id, ra.id, re.id) "reconciled_association_id", 22 | COALESCE(rs.name, COALESCE(ra.alias, ra.name), COALESCE(re.alias, re.name)) "reconciled_association_name", 23 | CASE WHEN rs.id IS NOT NULL THEN 'Allocated' ELSE COALESCE(rat.description, ret.description) END "reconciled_transaction_desc", 24 | ROW_NUMBER() OVER( 25 | PARTITION BY r.customer_id, r.source_transaction_id 26 | ) "rown" 27 | FROM public.account_reconciliation r 28 | LEFT JOIN account_transaction rat on r.target_account_transaction_id = rat.id 29 | LEFT JOIN account ra on rat.account_id = ra.id 30 | LEFT JOIN envelope_transaction ret on r.target_envelope_transaction_id = ret.id 31 | LEFT JOIN envelope re on ret.envelope_id = re.id 32 | LEFT JOIN plan rs ON rs.id = r.plan_id 33 | ) r ON r.customer_id = t.customer_id AND r.source_transaction_id = t.id AND rown = 1 34 | ORDER BY account_id, transaction_date DESC, id DESC; 35 | -------------------------------------------------------------------------------- /tests/data/tree/simple_plpgsql.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "PLpgSQL_function": { 4 | "action": { 5 | "PLpgSQL_stmt_block": { 6 | "body": [ 7 | { 8 | "PLpgSQL_stmt_if": { 9 | "cond": { 10 | "PLpgSQL_expr": { 11 | "parseMode": 2, 12 | "query": "v_version IS NULL" 13 | } 14 | }, 15 | "lineno": 1, 16 | "then_body": [ 17 | { 18 | "PLpgSQL_stmt_return": { 19 | "expr": { 20 | "PLpgSQL_expr": { 21 | "parseMode": 2, 22 | "query": "v_name" 23 | } 24 | }, 25 | "lineno": 1 26 | } 27 | } 28 | ] 29 | } 30 | }, 31 | { 32 | "PLpgSQL_stmt_return": { 33 | "expr": { 34 | "PLpgSQL_expr": { 35 | "parseMode": 2, 36 | "query": "v_name || '/' || v_version" 37 | } 38 | }, 39 | "lineno": 1 40 | } 41 | } 42 | ], 43 | "lineno": 1 44 | } 45 | }, 46 | "datums": [ 47 | { 48 | "PLpgSQL_var": { 49 | "datatype": { 50 | "PLpgSQL_type": { 51 | "typname": "pg_catalog.\"varchar\"" 52 | } 53 | }, 54 | "refname": "v_name" 55 | } 56 | }, 57 | { 58 | "PLpgSQL_var": { 59 | "datatype": { 60 | "PLpgSQL_type": { 61 | "typname": "pg_catalog.\"varchar\"" 62 | } 63 | }, 64 | "refname": "v_version" 65 | } 66 | }, 67 | { 68 | "PLpgSQL_var": { 69 | "datatype": { 70 | "PLpgSQL_type": { 71 | "typname": "pg_catalog.\"boolean\"" 72 | } 73 | }, 74 | "refname": "found" 75 | } 76 | } 77 | ] 78 | } 79 | } 80 | ] -------------------------------------------------------------------------------- /tests/fingerprint_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn it_can_fingerprint_a_simple_statement() { 3 | let result = pg_parse::fingerprint("SELECT * FROM contacts.person WHERE id IN (1, 2, 3, 4);"); 4 | assert!(result.is_ok()); 5 | let result = result.unwrap(); 6 | assert_eq!(result.hex, "643d2a3c294ab8a7"); 7 | } 8 | 9 | #[test] 10 | fn it_will_error_on_invalid_input() { 11 | let result = pg_parse::fingerprint("CREATE RANDOM ix_test ON contacts.person;"); 12 | assert!(result.is_err()); 13 | assert_eq!( 14 | result.err().unwrap(), 15 | pg_parse::Error::ParseError("syntax error at or near \"RANDOM\"".into()) 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /tests/normalize_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn it_can_normalize_a_simple_statement() { 3 | let result = pg_parse::normalize("SELECT * FROM contacts.person WHERE id IN (1, 2, 3, 4);"); 4 | assert!(result.is_ok()); 5 | let result = result.unwrap(); 6 | assert_eq!( 7 | result, 8 | "SELECT * FROM contacts.person WHERE id IN ($1, $2, $3, $4);" 9 | ); 10 | } 11 | 12 | #[test] 13 | fn it_will_error_on_invalid_input() { 14 | let result = pg_parse::normalize("CREATE RANDOM ix_test ON contacts.person;"); 15 | assert!(result.is_err()); 16 | assert_eq!( 17 | result.err().unwrap(), 18 | pg_parse::Error::ParseError("syntax error at or near \"RANDOM\"".into()) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /tests/parse_plpgsql_tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn it_can_parse_a_simple_function() { 3 | let result = pg_parse::parse_plpgsql( 4 | " \ 5 | CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name varchar, v_version varchar) \ 6 | RETURNS varchar AS $$ \ 7 | BEGIN \ 8 | IF v_version IS NULL THEN \ 9 | RETURN v_name; \ 10 | END IF; \ 11 | RETURN v_name || '/' || v_version; \ 12 | END; \ 13 | $$ LANGUAGE plpgsql;", 14 | ); 15 | assert!(result.is_ok()); 16 | let result = result.unwrap(); 17 | let expected = include_str!("data/tree/simple_plpgsql.json"); 18 | assert_eq!(serde_json::to_string_pretty(&result).unwrap(), expected); 19 | } 20 | 21 | #[test] 22 | fn it_will_error_on_invalid_input() { 23 | let result = pg_parse::parse_plpgsql("CREATE RANDOM ix_test ON contacts.person;"); 24 | assert!(result.is_err()); 25 | assert_eq!( 26 | result.err().unwrap(), 27 | pg_parse::Error::ParseError("syntax error at or near \"RANDOM\"".into()) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /tests/parse_tests.rs: -------------------------------------------------------------------------------- 1 | use pg_parse::ast::{ConstValue, ConstrType, InsertStmt, List, Node, ParamRef, SelectStmt}; 2 | 3 | #[test] 4 | fn it_can_generate_a_create_index_ast() { 5 | let result = 6 | pg_parse::parse("CREATE INDEX ix_test ON contacts.person (id, ssn) WHERE ssn IS NOT NULL;"); 7 | assert!(result.is_ok()); 8 | let result = result.unwrap(); 9 | let el: &Node = &result[0]; 10 | match *el { 11 | Node::IndexStmt(ref stmt) => { 12 | assert_eq!(stmt.idxname, Some("ix_test".to_string()), "idxname"); 13 | let relation = stmt.relation.as_ref().expect("relation exists"); 14 | assert_eq!( 15 | relation.schemaname, 16 | Some("contacts".to_string()), 17 | "schemaname" 18 | ); 19 | assert_eq!(relation.relname, Some("person".to_string()), "relname"); 20 | let params = stmt.index_params.as_ref().expect("index params"); 21 | assert_eq!(2, params.len(), "Params length"); 22 | } 23 | _ => panic!("Unexpected type"), 24 | } 25 | } 26 | 27 | #[test] 28 | fn it_can_generate_a_create_table_ast() { 29 | let result = pg_parse::parse( 30 | "CREATE TABLE contacts.person(id serial primary key, name text not null, balance numeric(5, 12));", 31 | ); 32 | assert!(result.is_ok()); 33 | let result = result.unwrap(); 34 | let el: &Node = &result[0]; 35 | match *el { 36 | Node::CreateStmt(ref stmt) => { 37 | let relation = stmt.relation.as_ref().expect("relation exists"); 38 | assert_eq!( 39 | relation.schemaname, 40 | Some("contacts".to_string()), 41 | "schemaname" 42 | ); 43 | assert_eq!(relation.relname, Some("person".to_string()), "relname"); 44 | let columns = stmt.table_elts.as_ref().expect("columns"); 45 | assert_eq!(3, columns.len(), "Columns length"); 46 | let balance = &columns[2]; 47 | let column = match balance { 48 | Node::ColumnDef(def) => def, 49 | _ => panic!("Unexpected column type"), 50 | }; 51 | assert_eq!(column.colname, Some("balance".into())); 52 | let ty = match &column.type_name { 53 | Some(t) => t, 54 | None => panic!("Missing type for column balance"), 55 | }; 56 | 57 | // Check the name of the type, and the modifiers 58 | let names = match &ty.names { 59 | Some(n) => n, 60 | None => panic!("No type names found"), 61 | }; 62 | assert_eq!(names.len(), 2, "Names length"); 63 | match &names[0] { 64 | Node::String { sval: value } => assert_eq!(value, &Some("pg_catalog".into())), 65 | unexpected => panic!("Unexpected type for name[0] {:?}", unexpected), 66 | } 67 | match &names[1] { 68 | Node::String { sval: value } => assert_eq!(value, &Some("numeric".into())), 69 | unexpected => panic!("Unexpected type for name[1] {:?}", unexpected), 70 | } 71 | 72 | // Do the mods 73 | let mods = match &ty.typmods { 74 | Some(m) => m, 75 | None => panic!("No type mods found"), 76 | }; 77 | assert_eq!(mods.len(), 2, "Mods length"); 78 | match &mods[0] { 79 | Node::A_Const(ConstValue::Integer(value)) => { 80 | assert_eq!(*value, 5); 81 | } 82 | unexpected => panic!("Unexpected type for mods[0] {:?}", unexpected), 83 | } 84 | match &mods[1] { 85 | Node::A_Const(ConstValue::Integer(value)) => { 86 | assert_eq!(*value, 12); 87 | } 88 | unexpected => panic!("Unexpected type for mods[0] {:?}", unexpected), 89 | } 90 | } 91 | _ => panic!("Unexpected type"), 92 | } 93 | } 94 | 95 | #[test] 96 | fn it_will_error_on_invalid_input() { 97 | let result = pg_parse::parse("CREATE RANDOM ix_test ON contacts.person;"); 98 | assert!(result.is_err()); 99 | assert_eq!( 100 | result.err().unwrap(), 101 | pg_parse::Error::ParseError("syntax error at or near \"RANDOM\"".into()) 102 | ); 103 | } 104 | 105 | #[test] 106 | fn it_can_parse_lists_of_values() { 107 | let result = pg_parse::parse("INSERT INTO contacts.person(name, ssn) VALUES ($1, $2)"); 108 | assert!(result.is_ok()); 109 | let result = result.unwrap(); 110 | let el: &Node = &result[0]; 111 | 112 | match el { 113 | Node::InsertStmt(InsertStmt { 114 | select_stmt: Some(select_stmt), 115 | .. 116 | }) => match select_stmt.as_ref() { 117 | Node::SelectStmt(SelectStmt { 118 | values_lists: Some(values_lists), 119 | .. 120 | }) => { 121 | let values = &values_lists[0]; 122 | 123 | match values { 124 | Node::List(List { items }) => { 125 | assert_eq!(2, items.len(), "Items length"); 126 | 127 | for (index, item) in items.iter().enumerate() { 128 | match item { 129 | Node::ParamRef(ParamRef { number, .. }) => { 130 | // postgres params indices start at 1 131 | let expected = index + 1; 132 | 133 | assert_eq!(expected, *number as usize, "Param number"); 134 | } 135 | node => panic!("Unexpected type {:#?}", &node), 136 | } 137 | } 138 | } 139 | node => panic!("Unexpected type {:#?}", &node), 140 | } 141 | } 142 | node => panic!("Unexpected type {:#?}", &node), 143 | }, 144 | node => panic!("Unexpected type {:#?}", &node), 145 | } 146 | } 147 | 148 | #[test] 149 | fn it_can_parse_a_table_of_defaults() { 150 | let result = pg_parse::parse( 151 | "CREATE TABLE default_values 152 | ( 153 | id serial NOT NULL PRIMARY KEY, 154 | ival int NOT NULL DEFAULT(1), 155 | bval boolean NOT NULL DEFAULT(TRUE), 156 | sval text NOT NULL DEFAULT('hello'), 157 | mval numeric(10,2) NOT NULL DEFAULT(5.12), 158 | nval int NULL DEFAULT(NULL) 159 | );", 160 | ); 161 | assert!(result.is_ok()); 162 | let result = result.unwrap(); 163 | let el: &Node = &result[0]; 164 | match *el { 165 | Node::CreateStmt(ref stmt) => { 166 | let relation = stmt.relation.as_ref().expect("relation exists"); 167 | assert_eq!(relation.schemaname, None, "schemaname"); 168 | assert_eq!( 169 | relation.relname, 170 | Some("default_values".to_string()), 171 | "relname" 172 | ); 173 | let columns = stmt.table_elts.as_ref().expect("columns"); 174 | assert_eq!(6, columns.len(), "Columns length"); 175 | let nval = &columns[5]; 176 | let column = match nval { 177 | Node::ColumnDef(def) => def, 178 | _ => panic!("Unexpected column type"), 179 | }; 180 | assert_eq!(column.colname, Some("nval".into())); 181 | assert!(column.constraints.is_some()); 182 | let constraints = column.constraints.as_ref().unwrap(); 183 | assert_eq!(2, constraints.len(), "constraint #"); 184 | let c1 = match &constraints[0] { 185 | Node::Constraint(c) => c, 186 | _ => panic!("Unexpected constraint type"), 187 | }; 188 | let c2 = match &constraints[1] { 189 | Node::Constraint(c) => c, 190 | _ => panic!("Unexpected constraint type"), 191 | }; 192 | assert_eq!(*c1.contype, ConstrType::CONSTR_NULL); 193 | assert_eq!(*c2.contype, ConstrType::CONSTR_DEFAULT); 194 | assert!(c2.raw_expr.is_some()); 195 | let raw_expr = c2.raw_expr.as_ref().unwrap(); 196 | let value = match **raw_expr { 197 | Node::A_Const(ref value) => value, 198 | _ => panic!("Expected constant value"), 199 | }; 200 | assert_eq!(*value, ConstValue::Null, "Expected NULL"); 201 | } 202 | _ => panic!("Unexpected type"), 203 | } 204 | } 205 | 206 | #[test] 207 | fn it_can_parse_tests() { 208 | // This is a set of tests inspired by libpg_query that test various situations. The scenario that 209 | // inspired this was actually SELECT DISTINCT, since it libpg_query it'll return [{}] which doesn't 210 | // have enough information to be parsed by pg_parse. We ignore empty array components like this. 211 | const TESTS: [(&str, &str); 26] = [ 212 | ( 213 | "SELECT 1", 214 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(A_Const(Integer(1))), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 215 | ), 216 | ( 217 | "SELECT 1; SELECT 2", 218 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(A_Const(Integer(1))), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None }), SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(A_Const(Integer(2))), location: 17 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 219 | ), 220 | ( 221 | "select sum(unique1) FILTER (WHERE unique1 IN (SELECT unique1 FROM onek where unique1 < 100)) FROM tenk1", 222 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(FuncCall(FuncCall { funcname: Some([String { sval: Some(\"sum\") }]), args: Some([ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 11 })]), agg_order: None, agg_filter: Some(SubLink(SubLink { sub_link_type: ANY_SUBLINK, sub_link_id: 0, testexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 34 })), oper_name: None, subselect: Some(SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 53 })), location: 53 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"onek\"), inh: true, relpersistence: 'p', alias: None, location: 66 })]), where_clause: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"<\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 77 })), rexpr: Some(A_Const(Integer(100))), location: 85 })), group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })), location: 42 })), over: None, agg_within_group: false, agg_star: false, agg_distinct: false, func_variadic: false, funcformat: COERCE_EXPLICIT_CALL, location: 7 })), location: 7 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"tenk1\"), inh: true, relpersistence: 'p', alias: None, location: 98 })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 223 | ), 224 | ( 225 | "select sum(unique1) FILTER (WHERE unique1 = ANY (SELECT unique1 FROM onek where unique1 < 100)) FROM tenk1", 226 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(FuncCall(FuncCall { funcname: Some([String { sval: Some(\"sum\") }]), args: Some([ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 11 })]), agg_order: None, agg_filter: Some(SubLink(SubLink { sub_link_type: ANY_SUBLINK, sub_link_id: 0, testexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 34 })), oper_name: Some([String { sval: Some(\"=\") }]), subselect: Some(SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 56 })), location: 56 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"onek\"), inh: true, relpersistence: 'p', alias: None, location: 69 })]), where_clause: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"<\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"unique1\") }]), location: 80 })), rexpr: Some(A_Const(Integer(100))), location: 88 })), group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })), location: 42 })), over: None, agg_within_group: false, agg_star: false, agg_distinct: false, func_variadic: false, funcformat: COERCE_EXPLICIT_CALL, location: 7 })), location: 7 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"tenk1\"), inh: true, relpersistence: 'p', alias: None, location: 101 })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 227 | ), 228 | ( 229 | "CREATE FOREIGN TABLE films (code char(5) NOT NULL, title varchar(40) NOT NULL, did integer NOT NULL, date_prod date, kind varchar(10), len interval hour to minute) SERVER film_server;", 230 | "[CreateForeignTableStmt(CreateForeignTableStmt { base: CreateStmt { relation: Some(RangeVar { catalogname: None, schemaname: None, relname: Some(\"films\"), inh: true, relpersistence: 'p', alias: None, location: 21 }), table_elts: Some([ColumnDef(ColumnDef { colname: Some(\"code\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"bpchar\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(5))]), typemod: -1, array_bounds: None, location: 33 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: Some([Constraint(Constraint { contype: CONSTR_NOTNULL, conname: None, deferrable: false, initdeferred: false, skip_validation: false, initially_valid: false, is_no_inherit: false, raw_expr: None, cooked_expr: None, generated_when: '\\0', inhcount: 0, nulls_not_distinct: false, keys: None, including: None, exclusions: None, options: None, indexname: None, indexspace: None, reset_default_tblspc: false, access_method: None, where_clause: None, pktable: None, fk_attrs: None, pk_attrs: None, fk_matchtype: '\\0', fk_upd_action: '\\0', fk_del_action: '\\0', fk_del_set_cols: None, old_conpfeqop: None, old_pktable_oid: 0, location: 41 })]), fdwoptions: None, location: 28 }), ColumnDef(ColumnDef { colname: Some(\"title\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"varchar\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(40))]), typemod: -1, array_bounds: None, location: 57 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: Some([Constraint(Constraint { contype: CONSTR_NOTNULL, conname: None, deferrable: false, initdeferred: false, skip_validation: false, initially_valid: false, is_no_inherit: false, raw_expr: None, cooked_expr: None, generated_when: '\\0', inhcount: 0, nulls_not_distinct: false, keys: None, including: None, exclusions: None, options: None, indexname: None, indexspace: None, reset_default_tblspc: false, access_method: None, where_clause: None, pktable: None, fk_attrs: None, pk_attrs: None, fk_matchtype: '\\0', fk_upd_action: '\\0', fk_del_action: '\\0', fk_del_set_cols: None, old_conpfeqop: None, old_pktable_oid: 0, location: 69 })]), fdwoptions: None, location: 51 }), ColumnDef(ColumnDef { colname: Some(\"did\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"int4\") }]), type_oid: 0, setof: false, pct_type: false, typmods: None, typemod: -1, array_bounds: None, location: 83 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: Some([Constraint(Constraint { contype: CONSTR_NOTNULL, conname: None, deferrable: false, initdeferred: false, skip_validation: false, initially_valid: false, is_no_inherit: false, raw_expr: None, cooked_expr: None, generated_when: '\\0', inhcount: 0, nulls_not_distinct: false, keys: None, including: None, exclusions: None, options: None, indexname: None, indexspace: None, reset_default_tblspc: false, access_method: None, where_clause: None, pktable: None, fk_attrs: None, pk_attrs: None, fk_matchtype: '\\0', fk_upd_action: '\\0', fk_del_action: '\\0', fk_del_set_cols: None, old_conpfeqop: None, old_pktable_oid: 0, location: 91 })]), fdwoptions: None, location: 79 }), ColumnDef(ColumnDef { colname: Some(\"date_prod\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"date\") }]), type_oid: 0, setof: false, pct_type: false, typmods: None, typemod: -1, array_bounds: None, location: 111 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: None, fdwoptions: None, location: 101 }), ColumnDef(ColumnDef { colname: Some(\"kind\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"varchar\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(10))]), typemod: -1, array_bounds: None, location: 122 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: None, fdwoptions: None, location: 117 }), ColumnDef(ColumnDef { colname: Some(\"len\"), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"interval\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(3072))]), typemod: -1, array_bounds: None, location: 139 }), compression: None, inhcount: 0, is_local: true, is_not_null: false, is_from_type: false, storage: '\\0', storage_name: None, raw_default: None, cooked_default: None, identity: '\\0', identity_sequence: None, generated: '\\0', coll_clause: None, coll_oid: 0, constraints: None, fdwoptions: None, location: 135 })]), inh_relations: None, partbound: None, partspec: None, of_typename: None, constraints: None, options: None, oncommit: ONCOMMIT_NOOP, tablespacename: None, access_method: None, if_not_exists: false }, servername: Some(\"film_server\"), options: None })]", 231 | ), 232 | ( 233 | "CREATE FOREIGN TABLE ft1 () SERVER no_server", 234 | "[CreateForeignTableStmt(CreateForeignTableStmt { base: CreateStmt { relation: Some(RangeVar { catalogname: None, schemaname: None, relname: Some(\"ft1\"), inh: true, relpersistence: 'p', alias: None, location: 21 }), table_elts: None, inh_relations: None, partbound: None, partspec: None, of_typename: None, constraints: None, options: None, oncommit: ONCOMMIT_NOOP, tablespacename: None, access_method: None, if_not_exists: false }, servername: Some(\"no_server\"), options: None })]", 235 | ), 236 | // ("SELECT parse_ident(E'\"c\".X XXXX\002XXXXXX')", ""), 237 | ( 238 | "ALTER ROLE postgres LOGIN SUPERUSER PASSWORD 'xyz'", 239 | "[AlterRoleStmt(AlterRoleStmt { role: Some(RoleSpec { roletype: ROLESPEC_CSTRING, rolename: Some(\"postgres\"), location: 11 }), options: Some([DefElem(DefElem { defnamespace: None, defname: Some(\"canlogin\"), arg: Some(Boolean { boolval: Some(true) }), defaction: DEFELEM_UNSPEC, location: 20 }), DefElem(DefElem { defnamespace: None, defname: Some(\"superuser\"), arg: Some(Boolean { boolval: Some(true) }), defaction: DEFELEM_UNSPEC, location: 26 }), DefElem(DefElem { defnamespace: None, defname: Some(\"password\"), arg: Some(String { sval: Some(\"xyz\") }), defaction: DEFELEM_UNSPEC, location: 36 })]), action: 1 })]", 240 | ), 241 | ( 242 | "SELECT extract($1 FROM $2)", 243 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(FuncCall(FuncCall { funcname: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"extract\") }]), args: Some([ParamRef(ParamRef { number: 1, location: 15 }), ParamRef(ParamRef { number: 2, location: 23 })]), agg_order: None, agg_filter: None, over: None, agg_within_group: false, agg_star: false, agg_distinct: false, func_variadic: false, funcformat: COERCE_SQL_SYNTAX, location: 7 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 244 | ), 245 | ( 246 | "WITH w AS NOT MATERIALIZED (SELECT * FROM big_table) SELECT * FROM w LIMIT 1", 247 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([A_Star(A_Star)]), location: 60 })), location: 60 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"w\"), inh: true, relpersistence: 'p', alias: None, location: 67 })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: Some(A_Const(Integer(1))), limit_option: LIMIT_OPTION_COUNT, locking_clause: None, with_clause: Some(WithClause { ctes: Some([CommonTableExpr(CommonTableExpr { ctename: Some(\"w\"), aliascolnames: None, ctematerialized: CTEMaterializeNever, ctequery: Some(SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([A_Star(A_Star)]), location: 35 })), location: 35 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"big_table\"), inh: true, relpersistence: 'p', alias: None, location: 42 })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })), search_clause: None, cycle_clause: None, location: 5, cterecursive: false, cterefcount: 0, ctecolnames: None, ctecoltypes: None, ctecoltypmods: None, ctecolcollations: None })]), recursive: false, location: 0 }), op: SETOP_NONE, all: false, larg: None, rarg: None })]", 248 | ), 249 | ( 250 | "CREATE USER test PASSWORD $1", 251 | "[CreateRoleStmt(CreateRoleStmt { stmt_type: ROLESTMT_USER, role: Some(\"test\"), options: Some([DefElem(DefElem { defnamespace: None, defname: Some(\"password\"), arg: Some(ParamRef(ParamRef { number: 1, location: 26 })), defaction: DEFELEM_UNSPEC, location: 17 })]) })]", 252 | ), 253 | ( 254 | "ALTER USER test ENCRYPTED PASSWORD $2", 255 | "[AlterRoleStmt(AlterRoleStmt { role: Some(RoleSpec { roletype: ROLESPEC_CSTRING, rolename: Some(\"test\"), location: 11 }), options: Some([DefElem(DefElem { defnamespace: None, defname: Some(\"password\"), arg: Some(ParamRef(ParamRef { number: 2, location: 35 })), defaction: DEFELEM_UNSPEC, location: 16 })]), action: 1 })]", 256 | ), 257 | ( 258 | "SET SCHEMA $3", 259 | "[VariableSetStmt(VariableSetStmt { kind: VAR_SET_VALUE, name: Some(\"search_path\"), args: Some([ParamRef(ParamRef { number: 3, location: 11 })]), is_local: false })]", 260 | ), 261 | ( 262 | "SET ROLE $4", 263 | "[VariableSetStmt(VariableSetStmt { kind: VAR_SET_VALUE, name: Some(\"role\"), args: Some([ParamRef(ParamRef { number: 4, location: 9 })]), is_local: false })]", 264 | ), 265 | ( 266 | "SET SESSION AUTHORIZATION $5", 267 | "[VariableSetStmt(VariableSetStmt { kind: VAR_SET_VALUE, name: Some(\"session_authorization\"), args: Some([ParamRef(ParamRef { number: 5, location: 26 })]), is_local: false })]", 268 | ), 269 | ( 270 | "SELECT EXTRACT($1 FROM TIMESTAMP $2)", 271 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(FuncCall(FuncCall { funcname: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"extract\") }]), args: Some([ParamRef(ParamRef { number: 1, location: 15 }), TypeCast(TypeCast { arg: Some(ParamRef(ParamRef { number: 2, location: 33 })), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"timestamp\") }]), type_oid: 0, setof: false, pct_type: false, typmods: None, typemod: -1, array_bounds: None, location: 23 }), location: -1 })]), agg_order: None, agg_filter: None, over: None, agg_within_group: false, agg_star: false, agg_distinct: false, func_variadic: false, funcformat: COERCE_SQL_SYNTAX, location: 7 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 272 | ), 273 | ( 274 | "SELECT DATE $1", 275 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(TypeCast(TypeCast { arg: Some(ParamRef(ParamRef { number: 1, location: 12 })), type_name: Some(TypeName { names: Some([String { sval: Some(\"date\") }]), type_oid: 0, setof: false, pct_type: false, typmods: None, typemod: -1, array_bounds: None, location: 7 }), location: -1 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 276 | ), 277 | ( 278 | "SELECT INTERVAL $1", 279 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(TypeCast(TypeCast { arg: Some(ParamRef(ParamRef { number: 1, location: 16 })), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"interval\") }]), type_oid: 0, setof: false, pct_type: false, typmods: None, typemod: -1, array_bounds: None, location: 7 }), location: -1 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 280 | ), 281 | ( 282 | "SELECT INTERVAL $1 YEAR", 283 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(TypeCast(TypeCast { arg: Some(ParamRef(ParamRef { number: 1, location: 16 })), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"interval\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(4))]), typemod: -1, array_bounds: None, location: 7 }), location: -1 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 284 | ), 285 | ( 286 | "SELECT INTERVAL (6) $1", 287 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(TypeCast(TypeCast { arg: Some(ParamRef(ParamRef { number: 1, location: 20 })), type_name: Some(TypeName { names: Some([String { sval: Some(\"pg_catalog\") }, String { sval: Some(\"interval\") }]), type_oid: 0, setof: false, pct_type: false, typmods: Some([A_Const(Integer(32767)), A_Const(Integer(6))]), typemod: -1, array_bounds: None, location: 7 }), location: -1 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 288 | ), 289 | ( 290 | "SET search_path = $1", 291 | "[VariableSetStmt(VariableSetStmt { kind: VAR_SET_VALUE, name: Some(\"search_path\"), args: Some([ParamRef(ParamRef { number: 1, location: 18 })]), is_local: false })]", 292 | ), 293 | ( 294 | "ALTER ROLE postgres LOGIN SUPERUSER PASSWORD $1", 295 | "[AlterRoleStmt(AlterRoleStmt { role: Some(RoleSpec { roletype: ROLESPEC_CSTRING, rolename: Some(\"postgres\"), location: 11 }), options: Some([DefElem(DefElem { defnamespace: None, defname: Some(\"canlogin\"), arg: Some(Boolean { boolval: Some(true) }), defaction: DEFELEM_UNSPEC, location: 20 }), DefElem(DefElem { defnamespace: None, defname: Some(\"superuser\"), arg: Some(Boolean { boolval: Some(true) }), defaction: DEFELEM_UNSPEC, location: 26 }), DefElem(DefElem { defnamespace: None, defname: Some(\"password\"), arg: Some(ParamRef(ParamRef { number: 1, location: 45 })), defaction: DEFELEM_UNSPEC, location: 36 })]), action: 1 })]", 296 | ), 297 | ( 298 | "WITH a AS (SELECT * FROM x WHERE x.y = $1 AND x.z = 1) SELECT * FROM a WHERE b = 5", 299 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([A_Star(A_Star)]), location: 62 })), location: 62 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"a\"), inh: true, relpersistence: 'p', alias: None, location: 69 })]), where_clause: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"=\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"b\") }]), location: 77 })), rexpr: Some(A_Const(Integer(5))), location: 79 })), group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: Some(WithClause { ctes: Some([CommonTableExpr(CommonTableExpr { ctename: Some(\"a\"), aliascolnames: None, ctematerialized: CTEMaterializeDefault, ctequery: Some(SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([A_Star(A_Star)]), location: 18 })), location: 18 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"x\"), inh: true, relpersistence: 'p', alias: None, location: 25 })]), where_clause: Some(BoolExpr(BoolExpr { boolop: AND_EXPR, args: Some([A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"=\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"x\") }, String { sval: Some(\"y\") }]), location: 33 })), rexpr: Some(ParamRef(ParamRef { number: 1, location: 39 })), location: 37 }), A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"=\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"x\") }, String { sval: Some(\"z\") }]), location: 46 })), rexpr: Some(A_Const(Integer(1))), location: 50 })]), location: 42 })), group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })), search_clause: None, cycle_clause: None, location: 5, cterecursive: false, cterefcount: 0, ctecolnames: None, ctecoltypes: None, ctecoltypmods: None, ctecolcollations: None })]), recursive: false, location: 0 }), op: SETOP_NONE, all: false, larg: None, rarg: None })]", 300 | ), 301 | ( 302 | "SELECT count(*) from testjsonb WHERE j->'array' ? 'bar'", 303 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(FuncCall(FuncCall { funcname: Some([String { sval: Some(\"count\") }]), args: None, agg_order: None, agg_filter: None, over: None, agg_within_group: false, agg_star: true, agg_distinct: false, func_variadic: false, funcformat: COERCE_EXPLICIT_CALL, location: 7 })), location: 7 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"testjsonb\"), inh: true, relpersistence: 'p', alias: None, location: 21 })]), where_clause: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"?\") }]), lexpr: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"->\") }]), lexpr: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"j\") }]), location: 38 })), rexpr: Some(A_Const(String(\"array\"))), location: 39 })), rexpr: Some(A_Const(String(\"bar\"))), location: 49 })), group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 304 | ), 305 | ( 306 | "SELECT DISTINCT a FROM b", 307 | "[SelectStmt(SelectStmt { distinct_clause: Some([]), into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([String { sval: Some(\"a\") }]), location: 16 })), location: 16 })]), from_clause: Some([RangeVar(RangeVar { catalogname: None, schemaname: None, relname: Some(\"b\"), inh: true, relpersistence: 'p', alias: None, location: 23 })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 308 | ), 309 | ( 310 | "SELECT * FROM generate_series(1, 2)", 311 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(ColumnRef(ColumnRef { fields: Some([A_Star(A_Star)]), location: 7 })), location: 7 })]), from_clause: Some([RangeFunction(RangeFunction { lateral: false, ordinality: false, is_rowsfrom: false, functions: Some([List(List { items: [FuncCall(FuncCall { funcname: Some([String { sval: Some(\"generate_series\") }]), args: Some([A_Const(Integer(1)), A_Const(Integer(2))]), agg_order: None, agg_filter: None, over: None, agg_within_group: false, agg_star: false, agg_distinct: false, func_variadic: false, funcformat: COERCE_EXPLICIT_CALL, location: 14 })] })]), alias: None, coldeflist: None })]), where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 312 | ), 313 | ( 314 | "SELECT 1 + 1", 315 | "[SelectStmt(SelectStmt { distinct_clause: None, into_clause: None, target_list: Some([ResTarget(ResTarget { name: None, indirection: None, val: Some(A_Expr(A_Expr { kind: AEXPR_OP, name: Some([String { sval: Some(\"+\") }]), lexpr: Some(A_Const(Integer(1))), rexpr: Some(A_Const(Integer(1))), location: 9 })), location: 7 })]), from_clause: None, where_clause: None, group_clause: None, group_distinct: false, having_clause: None, window_clause: None, values_lists: None, sort_clause: None, limit_offset: None, limit_count: None, limit_option: LIMIT_OPTION_DEFAULT, locking_clause: None, with_clause: None, op: SETOP_NONE, all: false, larg: None, rarg: None })]", 316 | ), 317 | ]; 318 | 319 | for (expr, tree) in TESTS { 320 | println!("{}", expr); 321 | let parsed = pg_parse::parse_debug(expr); 322 | assert!( 323 | parsed.is_ok(), 324 | "Failed to parse: {}, {:?}", 325 | expr, 326 | parsed.err() 327 | ); 328 | let (stmt, debug) = parsed.unwrap(); 329 | assert_eq!( 330 | format!("{:?}", stmt), 331 | tree, 332 | "Expr: {}, Debug: {}", 333 | expr, 334 | debug 335 | ); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /tests/str_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "str")] 2 | mod tests { 3 | use regex::Regex; 4 | 5 | fn execute_tests(tests: [&str; N]) { 6 | for test in tests { 7 | let (tree, debug) = match pg_parse::parse_debug(test) { 8 | Ok((tree, debug)) => (tree, debug), 9 | Err(e) => panic!("Failed to parse: {}: \"{}\"", e, test), 10 | }; 11 | // println!("Tree: {:?}, Debug: {}", tree, debug); 12 | 13 | // Compare converting this back to a string 14 | let sql = tree 15 | .iter() 16 | .map(|stmt| stmt.to_string()) 17 | .collect::>() 18 | .join("; "); 19 | assert_eq!( 20 | test, sql, 21 | "expected <> generated to_string:\n\n{:?}\n\nDebug: {}\n\n", 22 | tree, debug, 23 | ); 24 | 25 | // Parse it back and compare the original trees 26 | let reparsed_tree = pg_parse::parse(&sql).unwrap(); 27 | assert_eq!(format!("{:?}", tree), format!("{:?}", reparsed_tree)); 28 | } 29 | } 30 | 31 | #[test] 32 | fn it_correctly_converts_to_string_for_select_tests() { 33 | let tests = [ 34 | "SELECT 1", 35 | "SELECT 1; SELECT 2", 36 | "SELECT 1 FROM t(1)", 37 | "SELECT a AS b FROM x WHERE y = 5 AND z = y", 38 | "SELECT FROM x WHERE y = 5 AND z = y", 39 | "SELECT a AS b FROM public.x WHERE y = 5 AND z = y", 40 | "SELECT DISTINCT a, b, * FROM c WHERE d = e", 41 | "SELECT DISTINCT ON (a) a, b FROM c", 42 | "SELECT * INTO films_recent FROM films WHERE date_prod >= '2002-01-01'", 43 | "SELECT current_timestamp", 44 | "SELECT current_time(2)", 45 | "SELECT memory_total_bytes, memory_swap_total_bytes - memory_swap_free_bytes AS swap, date_part($1, s.collected_at) AS collected_at FROM snapshots s INNER JOIN system_snapshots ON snapshot_id = s.id WHERE s.database_id = $2 AND s.collected_at >= $3 AND s.collected_at <= $4 ORDER BY collected_at ASC", 46 | "SELECT * FROM a ORDER BY x ASC NULLS FIRST", 47 | "SELECT * FROM a ORDER BY x ASC NULLS LAST", 48 | "SELECT * FROM a ORDER BY x COLLATE \"tr_TR\" DESC NULLS LAST", 49 | "SELECT 'foo' COLLATE \"tr_TR\"", 50 | "SELECT id, name FROM table1 UNION (SELECT id, name FROM table2 ORDER BY name) ORDER BY id ASC", 51 | "SELECT a FROM kodsis EXCEPT SELECT a FROM application", 52 | "SELECT * FROM (VALUES ('anne', 'smith'), ('bob', 'jones'), ('joe', 'blow')) names(first, last)", 53 | "SELECT * FROM users WHERE name LIKE 'postgresql:%'", 54 | "SELECT * FROM users WHERE name NOT LIKE 'postgresql:%'", 55 | "SELECT * FROM users WHERE name ILIKE 'postgresql:%'", 56 | "SELECT * FROM users WHERE name NOT ILIKE 'postgresql:%'", 57 | "SELECT OVERLAY(m.name PLACING '******' FROM 3 FOR 6) AS tc_kimlik FROM tb_test m", 58 | "SELECT sum(price_cents) FROM products", 59 | "SELECT ARRAY(SELECT id FROM products)::bigint[]", 60 | "SELECT m.name AS mname, pname FROM manufacturers m, LATERAL get_product_names(m.id) pname", 61 | "SELECT m.name AS mname, pname FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON TRUE", 62 | "SELECT * FROM tb_test_main mh INNER JOIN LATERAL (SELECT ftnrm.* FROM test ftnrm WHERE ftnrm.hizmet_id = mh.id UNION ALL SELECT ftarc.* FROM test.test2 ftarc WHERE ftarc.hizmet_id = mh.id) ft ON TRUE", 63 | "SELECT x, y FROM a CROSS JOIN b", 64 | "SELECT x, y FROM a NATURAL INNER JOIN b", 65 | "SELECT x, y FROM a LEFT JOIN b ON 1 > 0", 66 | "SELECT x, y FROM a RIGHT JOIN b ON 1 > 0", 67 | "SELECT x, y FROM a FULL JOIN b ON 1 > 0", 68 | "SELECT x, y FROM a INNER JOIN b USING (z)", 69 | "SELECT 2 + 2", 70 | "SELECT * FROM x WHERE y IS NULL", 71 | "SELECT * FROM x WHERE y IS NOT NULL", 72 | "SELECT count(*) FROM x WHERE y IS NOT NULL", 73 | "SELECT count(DISTINCT a) FROM x WHERE y IS NOT NULL", 74 | "SELECT CASE WHEN a.status = 1 THEN 'active' WHEN a.status = 2 THEN 'inactive' END FROM accounts a", 75 | "SELECT CASE 1 > 0 WHEN TRUE THEN 'ok' ELSE NULL END", 76 | "SELECT CASE WHEN a.status = 1 THEN 'active' WHEN a.status = 2 THEN 'inactive' ELSE 'unknown' END FROM accounts a", 77 | "SELECT * FROM accounts WHERE status = CASE WHEN x = 1 THEN 'active' ELSE 'inactive' END", 78 | "SELECT CASE WHEN EXISTS (SELECT 1) THEN 1 ELSE 2 END", 79 | "SELECT (SELECT 'x')", 80 | "SELECT * FROM (SELECT generate_series(0, 100)) a", 81 | "SELECT * FROM x WHERE id IN (1, 2, 3)", 82 | "SELECT * FROM x WHERE id IN (SELECT id FROM account)", 83 | "SELECT * FROM x WHERE id NOT IN (1, 2, 3)", 84 | "SELECT * FROM x INNER JOIN (SELECT n FROM z) b ON a.id = b.id", 85 | "SELECT * FROM x WHERE y = z[$1]", 86 | "SELECT (foo(1)).y", 87 | "SELECT proname, (SELECT regexp_split_to_array(proargtypes::text, ' '))[idx] AS argtype, proargnames[idx] AS argname FROM pg_proc", 88 | "SELECT COALESCE((SELECT customer.sp_person(n.id) AS sp_person).city_id, NULL::int) AS city_id FROM customer.tb_customer n", 89 | "SELECT * FROM x WHERE y = z[$1][$2]", 90 | "SELECT (k #= hstore('{id}'::text[], ARRAY[1::text])).* FROM test k", 91 | "SELECT * FROM x WHERE NOT y", 92 | "SELECT * FROM x WHERE x OR y", 93 | "SELECT 1 WHERE (1 = 1 OR 1 = 2) AND 1 = 2", 94 | "SELECT 1 WHERE (1 = 1 AND 2 = 2) OR 2 = 3", 95 | "SELECT 1 WHERE 1 = 1 OR 2 = 2 OR 2 = 3", 96 | "SELECT * FROM x WHERE x = ALL($1)", 97 | "SELECT * FROM x WHERE x = ANY($1)", 98 | "SELECT * FROM x WHERE x = COALESCE(y, $1)", 99 | "SELECT a, b, max(c) FROM c WHERE d = 1 GROUP BY a, b", 100 | "SELECT * FROM x LIMIT 50", 101 | "SELECT * FROM x OFFSET 50", 102 | "SELECT amount * 0.5", 103 | "SELECT * FROM x WHERE x BETWEEN '2016-01-01' AND '2016-02-02'", 104 | "SELECT * FROM x WHERE x NOT BETWEEN '2016-01-01' AND '2016-02-02'", 105 | "SELECT * FROM x WHERE x BETWEEN SYMMETRIC 20 AND 10", 106 | "SELECT * FROM x WHERE x NOT BETWEEN SYMMETRIC 20 AND 10", 107 | "SELECT NULLIF(id, 0) AS id FROM x", 108 | "SELECT NULL FROM x", 109 | "SELECT * FROM x WHERE y IS TRUE", 110 | "SELECT * FROM x WHERE y IS NOT TRUE", 111 | "SELECT * FROM x WHERE y IS FALSE", 112 | "SELECT * FROM x WHERE y IS NOT FALSE", 113 | "SELECT * FROM x WHERE y IS UNKNOWN", 114 | "SELECT * FROM x WHERE y IS NOT UNKNOWN", 115 | "SELECT * FROM crosstab('SELECT department, role, COUNT(id) FROM users GROUP BY department, role ORDER BY department, role', 'VALUES (''admin''::text), (''ordinary''::text)') AS (department varchar, admin int, ordinary int)", 116 | "SELECT * FROM crosstab('SELECT department, role, COUNT(id) FROM users GROUP BY department, role ORDER BY department, role', 'VALUES (''admin''::text), (''ordinary''::text)') ctab (department varchar, admin int, ordinary int)", 117 | "SELECT row_cols[0] AS dept, row_cols[1] AS sub, admin, ordinary FROM crosstab('SELECT ARRAY[department, sub] AS row_cols, role, COUNT(id) FROM users GROUP BY department, role ORDER BY department, role', 'VALUES (''admin''::text), (''ordinary''::text)') AS (row_cols varchar[], admin int, ordinary int)", 118 | "SELECT 1::int8", 119 | "SELECT CAST(1 + 3 AS int8)", 120 | "SELECT $1::regclass", 121 | "SELECT table_field::bool, table_field::boolean FROM t", 122 | "SELECT TRUE, FALSE", 123 | "SELECT 1::boolean, 0::boolean", 124 | "SELECT $5", 125 | "SELECT $1", 126 | "SELECT * FROM people FOR UPDATE OF name, email", 127 | "SELECT name::varchar(255) FROM people", 128 | "SELECT name::varchar FROM people", 129 | "SELECT age::numeric(5, 2) FROM people", 130 | "SELECT age::numeric FROM people", 131 | "SELECT m.name AS mname, pname FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON TRUE", 132 | "SELECT * FROM a CROSS JOIN (b CROSS JOIN c)", 133 | "SELECT 1 FOR UPDATE", 134 | "SELECT 1 FOR UPDATE NOWAIT", 135 | "SELECT 1 FOR UPDATE SKIP LOCKED", 136 | "SELECT rank(*) OVER ()", 137 | "SELECT rank(*) OVER (PARTITION BY id)", 138 | "SELECT rank(*) OVER (ORDER BY id)", 139 | "SELECT rank(*) OVER (PARTITION BY id, id2 ORDER BY id DESC, id2)", 140 | "SELECT rank(*) OVER named_window", 141 | "SELECT max(create_date::date) FILTER (WHERE cancel_date IS NULL) OVER (ORDER BY create_date DESC) FROM tb_x", 142 | "SELECT ROW(1 + 2)", 143 | "SELECT (3 + 3) OPERATOR(pg_catalog.*) 2", 144 | "SELECT 3 + (3 * 2)", 145 | "SELECT LIMIT ALL", 146 | "SELECT * FROM ROWS FROM (foo() AS (foo_res_a text COLLATE a, foo_res_b text))", 147 | "SELECT 1 FROM a.b.c", 148 | "SELECT sum(unique1) FILTER (WHERE unique1 IN (SELECT unique1 FROM onek WHERE unique1 < 100)) FROM tenk1", 149 | "SELECT customer_id, sum(amount) FROM payment GROUP BY customer_id HAVING sum(amount) > 200", 150 | "SELECT *, lag(emp_salary, 1) OVER (ORDER BY emp_salary ASC) AS previous_salary FROM employee", 151 | "SELECT *, lead(emp_salary, 1) OVER (ORDER BY emp_salary ASC) AS previous_salary FROM employee", 152 | "SELECT emp_id, emp_salary, emp_address, rank() OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 153 | "SELECT emp_id, emp_salary, emp_address, row_number() OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 154 | "SELECT emp_id, emp_salary, emp_address, dense_rank() OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 155 | "SELECT emp_id, emp_salary, emp_address, ntile(1) OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 156 | "SELECT emp_id, emp_salary, emp_address, percent_rank() OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 157 | "SELECT emp_id, emp_salary, emp_address, cume_dist() OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 158 | "SELECT emp_id, emp_salary, emp_address, first_value(emp_id) OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 159 | "SELECT emp_id, emp_salary, emp_address, last_value(emp_id) OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 160 | "SELECT emp_id, emp_salary, emp_address, nth_value(emp_salary, 2) OVER (PARTITION BY emp_address ORDER BY emp_salary DESC) FROM employee", 161 | "SELECT film_id, title, length INTO TEMPORARY short_film FROM film WHERE length < 60 ORDER BY title", 162 | "SELECT film_id, title, length INTO UNLOGGED short_film FROM film WHERE length < 60 ORDER BY title", 163 | "SELECT group_name, avg(price) FROM products INNER JOIN product_groups USING (group_id) GROUP BY group_name", 164 | "SELECT product_name, price, group_name, avg(price) OVER (PARTITION BY group_name) FROM products INNER JOIN product_groups USING (group_id)", 165 | "SELECT wf1() OVER (PARTITION BY c1 ORDER BY c2), wf2() OVER (PARTITION BY c1 ORDER BY c2) FROM table_name", 166 | "SELECT wf1() OVER w, wf2() OVER w FROM table_name WINDOW w AS (PARTITION BY c1 ORDER BY c2)", 167 | "SELECT product_name, group_name, price, row_number() OVER (PARTITION BY group_name ORDER BY price) FROM products INNER JOIN product_groups USING (group_id)", 168 | "SELECT product_name, group_name, price, rank() OVER (PARTITION BY group_name ORDER BY price) FROM products INNER JOIN product_groups USING (group_id)", 169 | "SELECT product_name, group_name, price, dense_rank() OVER (PARTITION BY group_name ORDER BY price) FROM products INNER JOIN product_groups USING (group_id)", 170 | "SELECT product_name, group_name, price, first_value(price) OVER (PARTITION BY group_name ORDER BY price) AS lowest_price_per_group FROM products INNER JOIN product_groups USING (group_id)", 171 | "SELECT product_name, group_name, price, last_value(price) OVER (PARTITION BY group_name ORDER BY price RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS highest_price_per_group FROM products INNER JOIN product_groups USING (group_id)", 172 | "SELECT product_name, group_name, price, lag(price, 1) OVER (PARTITION BY group_name ORDER BY price) AS prev_price, price - lag(price, 1) OVER (PARTITION BY group_name ORDER BY price) AS cur_prev_diff FROM products INNER JOIN product_groups USING (group_id)", 173 | "SELECT product_name, group_name, price, lead(price, 1) OVER (PARTITION BY group_name ORDER BY price) AS next_price, price - lead(price, 1) OVER (PARTITION BY group_name ORDER BY price) AS cur_next_diff FROM products INNER JOIN product_groups USING (group_id)", 174 | "SELECT foo, bar FROM atable WHERE foo IS DISTINCT FROM bar", 175 | "SELECT foo, bar FROM atable WHERE foo IS NOT DISTINCT FROM bar", 176 | "SELECT t1.foo, t1.bar, t1.baz FROM t1 LEFT JOIN t2 ON t1.foo IS NOT DISTINCT FROM t2.foo AND t1.bar IS NOT DISTINCT FROM t2.bar AND t1.baz IS NOT DISTINCT FROM t2.baz WHERE t2.foo IS NULL", 177 | "SELECT country_name FROM countries WHERE (country_name SIMILAR TO 'New Zealand') = 't'", 178 | "SELECT country_name FROM countries WHERE country_name SIMILAR TO 'New Zealand' IS TRUE", 179 | "SELECT country_name FROM countries WHERE country_name SIMILAR TO 'New Zealand'", 180 | "SELECT country_name FROM countries WHERE country_name NOT SIMILAR TO 'New Zealand'", 181 | "SELECT location, sum(quantity) FROM sales GROUP BY ROLLUP (location)", 182 | "SELECT location, product, sum(quantity) FROM sales GROUP BY ROLLUP (location, product) ORDER BY location, product", 183 | "SELECT COALESCE(location, 'All locations') AS location, COALESCE(product, 'All products') AS product, sum(quantity) FROM sales GROUP BY ROLLUP (location, product) ORDER BY location, product", 184 | "SELECT COALESCE(location, 'All locations') AS location, COALESCE(product, 'All products') AS product, sum(quantity) FROM sales GROUP BY CUBE (location, product) ORDER BY location, product", 185 | "SELECT GROUPING(brand) AS grouping_brand, GROUPING(segment) AS grouping_segment, brand, segment, sum(quantity) FROM sales GROUP BY GROUPING SETS (brand, segment, ()) ORDER BY brand, segment", 186 | "SELECT GROUPING(brand) AS grouping_brand, GROUPING(segment) AS grouping_segment, brand, segment, sum(quantity) FROM sales GROUP BY GROUPING SETS (brand, segment, ()) HAVING GROUPING(brand) = 0 ORDER BY brand, segment", 187 | "SELECT film_id, title, length FROM film WHERE length > ALL (SELECT round(avg(length), 2) FROM film GROUP BY rating) ORDER BY length", 188 | "SELECT title, category_id FROM film INNER JOIN film_category USING (film_id) WHERE category_id = ANY (SELECT category_id FROM category WHERE name = 'Action' OR name = 'Drama')", 189 | "SELECT current_date", 190 | "SELECT current_time", 191 | "SELECT current_time(2)", 192 | "SELECT current_timestamp", 193 | "SELECT current_timestamp(0)", 194 | "SELECT localtime", 195 | "SELECT localtime(0)", 196 | "SELECT localtimestamp", 197 | "SELECT localtimestamp(2)", 198 | "SELECT current_catalog", 199 | "SELECT current_role", 200 | "SELECT current_schema", 201 | "SELECT current_user", 202 | "SELECT user", 203 | "SELECT session_user", 204 | "SELECT GREATEST(1, 2, 3, 4, 5)", 205 | "SELECT LEAST(1, 2, 3, 4, 5)", 206 | ]; 207 | execute_tests(tests) 208 | } 209 | 210 | #[test] 211 | fn it_correctly_converts_to_string_for_with_tests() { 212 | let tests = [ 213 | "WITH kodsis AS (SELECT * FROM application), kodsis2 AS (SELECT * FROM application) SELECT * FROM kodsis UNION SELECT * FROM kodsis ORDER BY id DESC", 214 | "WITH t AS (SELECT random() AS x FROM generate_series(1, 3)) SELECT * FROM t", 215 | "WITH RECURSIVE search_graph(id, link, data, depth, path, cycle) AS (SELECT g.id, g.link, g.data, 1, ARRAY[ROW(g.f1, g.f2)], FALSE FROM graph g UNION ALL SELECT g.id, g.link, g.data, sg.depth + 1, path || ROW(g.f1, g.f2), ROW(g.f1, g.f2) = ANY(path) FROM graph g, search_graph sg WHERE g.id = sg.link AND NOT cycle) SELECT id, data, link FROM search_graph", 216 | "WITH moved AS (DELETE FROM employees WHERE manager_name = 'Mary') INSERT INTO employees_of_mary SELECT * FROM moved", 217 | "WITH archived AS (DELETE FROM employees WHERE manager_name = 'Mary') UPDATE users SET archived = TRUE WHERE users.id IN (SELECT user_id FROM moved)", 218 | "WITH archived AS (DELETE FROM employees WHERE manager_name = 'Mary' RETURNING user_id) UPDATE users SET archived = TRUE FROM archived WHERE archived.user_id = id RETURNING id", 219 | "WITH archived AS (DELETE FROM employees WHERE manager_name = 'Mary') DELETE FROM users WHERE users.id IN (SELECT user_id FROM moved)", 220 | ]; 221 | execute_tests(tests) 222 | } 223 | 224 | #[test] 225 | fn it_correctly_converts_to_string_for_insert_tests() { 226 | let tests = [ 227 | "INSERT INTO x (y, z) VALUES (1, 'abc')", 228 | "INSERT INTO x (\"user\") VALUES ('abc')", 229 | "INSERT INTO x (y, z) VALUES (1, 'abc') RETURNING id", 230 | "INSERT INTO x SELECT * FROM y", 231 | "INSERT INTO x (y, z) VALUES (1, 'abc') ON CONFLICT (y) DO UPDATE SET \"user\" = excluded.\"user\" RETURNING y", 232 | "INSERT INTO x (y, z) VALUES (1, 'abc') ON CONFLICT (y) DO NOTHING RETURNING y", 233 | "INSERT INTO distributors (did, dname) VALUES (10, 'Conrad International') ON CONFLICT (did) WHERE is_active DO NOTHING", 234 | "INSERT INTO distributors (did, dname) VALUES (9, 'Antwerp Design') ON CONFLICT ON CONSTRAINT distributors_pkey DO NOTHING", 235 | "INSERT INTO employees SELECT * FROM people WHERE 1 = 1 GROUP BY name HAVING count(name) > 1 ORDER BY name DESC LIMIT 10 OFFSET 15 FOR UPDATE", 236 | "INSERT INTO films VALUES ('T_601', 'Yojimbo', 106, DEFAULT, 'Drama', DEFAULT)", 237 | "INSERT INTO jackdanger_card_totals (id, amount_cents, created_at) SELECT series.i, random() * 1000, (SELECT '2015-08-25 00:00:00 -0700'::timestamp + (('2015-08-25 23:59:59 -0700'::timestamp - '2015-08-25 00:00:00 -0700'::timestamp) * random())) FROM generate_series(1, 10000) series(i)", 238 | ]; 239 | execute_tests(tests) 240 | } 241 | 242 | #[test] 243 | fn it_correctly_converts_to_string_for_update_tests() { 244 | let tests = [ 245 | "UPDATE x SET y = 1 WHERE z = 'abc'", 246 | "UPDATE ONLY x table_x SET y = 1 WHERE z = 'abc' RETURNING y AS changed_y", 247 | "UPDATE foo SET a = $1, b = $2", 248 | "UPDATE x SET \"user\" = 'emin'", 249 | ]; 250 | 251 | execute_tests(tests) 252 | } 253 | 254 | #[test] 255 | fn it_correctly_converts_to_string_for_delete_tests() { 256 | let tests = [ 257 | "DELETE FROM x WHERE y = 1", 258 | "DELETE FROM ONLY x table_x USING table_z WHERE y = 1 RETURNING *", 259 | ]; 260 | 261 | execute_tests(tests) 262 | } 263 | 264 | // Merge support is not included yet 265 | // #[test] 266 | // fn it_correctly_converts_to_string_for_merge_tests() { 267 | // let tests = [ 268 | // "MERGE INTO customer_account ca USING recent_transactions t ON t.customer_id = ca.customer_id WHEN MATCHED THEN UPDATE SET balance = balance + transaction_value WHEN NOT MATCHED THEN INSERT (customer_id, balance) VALUES (t.customer_id, t.transaction_value)", 269 | // "MERGE INTO customer_account ca USING (SELECT customer_id, transaction_value FROM recent_transactions) AS t ON t.customer_id = ca.customer_id WHEN MATCHED THEN UPDATE SET balance = balance + transaction_value WHEN NOT MATCHED THEN INSERT (customer_id, balance) VALUES (t.customer_id, t.transaction_value)", 270 | // "MERGE INTO wines w USING wine_stock_changes s ON s.winename = w.winename WHEN NOT MATCHED AND s.stock_delta > 0 THEN INSERT VALUES(s.winename, s.stock_delta) WHEN MATCHED AND w.stock + s.stock_delta > 0 THEN UPDATE SET stock = w.stock + s.stock_delta WHEN MATCHED THEN DELETE" 271 | // ]; 272 | // 273 | // execute_tests(tests) 274 | // } 275 | 276 | #[test] 277 | fn it_correctly_converts_to_string_for_create_cast_tests() { 278 | let tests = [ 279 | "CREATE CAST (bigint AS int4) WITH FUNCTION int4(bigint) AS ASSIGNMENT", 280 | "CREATE CAST (bigint AS int4) WITHOUT FUNCTION AS IMPLICIT", 281 | "CREATE CAST (bigint AS int4) WITH INOUT AS ASSIGNMENT", 282 | ]; 283 | 284 | execute_tests(tests) 285 | } 286 | 287 | #[test] 288 | fn it_correctly_converts_to_string_for_create_domain_tests() { 289 | let tests = [ 290 | "CREATE DOMAIN us_postal_code AS text CHECK (\"VALUE\" ~ E'^\\\\d{5}$' OR \"VALUE\" ~ E'^\\\\d{5}-\\\\d{4}$')", 291 | ]; 292 | 293 | execute_tests(tests) 294 | } 295 | 296 | #[test] 297 | fn it_correctly_converts_to_string_for_create_function_tests() { 298 | let tests = [ 299 | "CREATE FUNCTION getfoo(int) RETURNS SETOF users AS $$SELECT * FROM \"users\" WHERE users.id = $1;$$ LANGUAGE sql", 300 | "CREATE OR REPLACE FUNCTION getfoo(int) RETURNS SETOF users AS $$SELECT * FROM \"users\" WHERE users.id = $1;$$ LANGUAGE sql", 301 | "CREATE OR REPLACE FUNCTION getfoo(int) RETURNS SETOF users AS $$SELECT * FROM \"users\" WHERE users.id = $1;$$ LANGUAGE sql IMMUTABLE", 302 | "CREATE OR REPLACE FUNCTION getfoo(int) RETURNS SETOF users AS $$SELECT * FROM \"users\" WHERE users.id = $1;$$ LANGUAGE sql IMMUTABLE RETURNS NULL ON NULL INPUT", 303 | "CREATE OR REPLACE FUNCTION getfoo(int) RETURNS SETOF users AS $$SELECT * FROM \"users\" WHERE users.id = $1;$$ LANGUAGE sql IMMUTABLE CALLED ON NULL INPUT", 304 | "CREATE OR REPLACE FUNCTION getfoo() RETURNS text AS $$SELECT name FROM \"users\" LIMIT 1$$ LANGUAGE sql IMMUTABLE CALLED ON NULL INPUT", 305 | "CREATE OR REPLACE FUNCTION getfoo() RETURNS text AS $$SELECT name FROM \"users\" LIMIT 1$$ LANGUAGE sql IMMUTABLE SECURITY DEFINER", 306 | "CREATE OR REPLACE FUNCTION getfoo() RETURNS text AS $$SELECT name FROM \"users\" LIMIT 1$$ LANGUAGE sql IMMUTABLE SECURITY INVOKER", 307 | "CREATE OR REPLACE FUNCTION getfoo() RETURNS text AS $$SELECT name FROM \"users\" LIMIT 1$$ LANGUAGE sql IMMUTABLE LEAKPROOF", 308 | "CREATE OR REPLACE FUNCTION getfoo() RETURNS text AS $$SELECT name FROM \"users\" LIMIT 1$$ LANGUAGE sql IMMUTABLE NOT LEAKPROOF", 309 | ]; 310 | 311 | execute_tests(tests) 312 | } 313 | 314 | #[test] 315 | fn it_correctly_converts_to_string_for_create_schema_tests() { 316 | let tests = [ 317 | "CREATE SCHEMA myschema", 318 | "CREATE SCHEMA AUTHORIZATION joe", 319 | "CREATE SCHEMA IF NOT EXISTS test AUTHORIZATION joe", 320 | "CREATE SCHEMA hollywood CREATE TABLE films (title text, release date, awards text[]) CREATE VIEW winners AS SELECT title, release FROM films WHERE awards IS NOT NULL", 321 | ]; 322 | execute_tests(tests) 323 | } 324 | 325 | #[test] 326 | fn it_correctly_converts_to_string_for_create_table_tests() { 327 | let tests = [ 328 | "CREATE UNLOGGED TABLE cities (name text, population real, altitude double, identifier smallint, postal_code int, foreign_id bigint)", 329 | "CREATE TABLE IF NOT EXISTS distributors (name varchar(40) DEFAULT 'Luso Films', len interval hour to second(3), name varchar(40) DEFAULT 'Luso Films', did int DEFAULT nextval('distributors_serial'), stamp timestamp DEFAULT now() NOT NULL, stamptz timestamp with time zone, time time NOT NULL, timetz time with time zone, CONSTRAINT name_len PRIMARY KEY (name, len))", 330 | "CREATE TABLE types (a real, b double precision, c numeric(2, 3), d char(4), e char(5), f varchar(6), g varchar(7))", 331 | "CREATE TABLE types (a geometry(point) NOT NULL)", 332 | "CREATE TABLE tablename (colname int NOT NULL DEFAULT nextval('tablename_colname_seq'))", 333 | "CREATE TABLE capitals (state char(2)) INHERITS (cities)", 334 | "CREATE TEMPORARY TABLE temp AS SELECT c FROM t", 335 | "CREATE TABLE films2 AS SELECT * FROM films", 336 | "CREATE TEMPORARY TABLE films_recent ON COMMIT DROP AS SELECT * FROM films WHERE date_prod > $1", 337 | "CREATE TABLE like_constraint_rename_cache (LIKE constraint_rename_cache INCLUDING ALL)", 338 | ]; 339 | execute_tests(tests) 340 | } 341 | 342 | #[test] 343 | fn it_correctly_converts_to_string_for_create_view_tests() { 344 | let tests = [ 345 | "CREATE OR REPLACE TEMPORARY VIEW view_a AS SELECT * FROM a(1) WITH CHECK OPTION", 346 | "CREATE VIEW view_a (a, b) AS WITH RECURSIVE view_a(a, b) AS (SELECT * FROM a(1)) SELECT a, b FROM view_a", 347 | ]; 348 | execute_tests(tests) 349 | } 350 | 351 | #[test] 352 | fn it_correctly_converts_to_string_for_create_misc_tests() { 353 | let tests = [ 354 | "CREATE AGGREGATE aggregate1 (int4) (sfunc = sfunc1, stype = stype1)", 355 | "CREATE AGGREGATE aggregate1 (int4, bool) (sfunc = sfunc1, stype = stype1)", 356 | "CREATE AGGREGATE aggregate1 (*) (sfunc = sfunc1, stype = stype1)", 357 | "CREATE AGGREGATE aggregate1 (int4) (sfunc = sfunc1, stype = stype1, finalfunc_extra, mfinalfuncextra)", 358 | "CREATE AGGREGATE aggregate1 (int4) (sfunc = sfunc1, stype = stype1, finalfunc_modify = read_only, parallel = restricted)", 359 | "CREATE AGGREGATE percentile_disc (float8 ORDER BY anyelement) (sfunc = ordered_set_transition, stype = internal, finalfunc = percentile_disc_final, finalfunc_extra)", 360 | "CREATE OPERATOR + (procedure = plusfunc)", 361 | "CREATE OPERATOR + (procedure = plusfunc, leftarg = int4, rightarg = int4)", 362 | "CREATE OPERATOR + (procedure = plusfunc, hashes, merges)", 363 | "CREATE TYPE type1", 364 | "CREATE TYPE type1 AS (attr1 int4, attr2 bool)", 365 | "CREATE TYPE type1 AS (attr1 int4 COLLATE collation1, attr2 bool)", 366 | "CREATE TYPE type1 AS ENUM ('value1', 'value2', 'value3')", 367 | "CREATE TYPE type1 AS RANGE (subtype = int4)", 368 | "CREATE TYPE type1 AS RANGE (subtype = int4, receive = receive_func, passedbyvalue)", 369 | "CREATE TYPE type1 (input = input1, output = output1)", 370 | "CREATE TYPE type1 (input = input1, output = output1, passedbyvalue)", 371 | "CREATE TABLESPACE x LOCATION 'a'", 372 | "CREATE TABLESPACE x OWNER a LOCATION 'b' WITH (random_page_cost=42, seq_page_cost=3)", 373 | ]; 374 | execute_tests(tests) 375 | } 376 | 377 | #[test] 378 | fn it_correctly_converts_to_string_for_drop_tests() { 379 | let tests = [ 380 | "DROP SERVER IF EXISTS foo", 381 | "DROP PUBLICATION mypublication", 382 | "DROP TYPE box", 383 | "DROP TABLESPACE mystuff", 384 | "DROP CONVERSION myname", 385 | "DROP SEQUENCE serial", 386 | "DROP MATERIALIZED VIEW order_summary", 387 | "DROP TRIGGER if_dist_exists ON films", 388 | "DROP RULE newrule ON mytable", 389 | "DROP CAST (text AS int)", 390 | "DROP OPERATOR FAMILY float_ops USING btree", 391 | "DROP AGGREGATE myavg(int), myavg(bigint)", 392 | "DROP COLLATION german", 393 | "DROP FOREIGN DATA WRAPPER dbi", 394 | "DROP ACCESS METHOD heptree", 395 | "DROP STATISTICS IF EXISTS accounting.users_uid_creation, public.grants_user_role", 396 | "DROP TEXT SEARCH DICTIONARY english", 397 | "DROP OPERATOR CLASS widget_ops USING btree", 398 | "DROP POLICY p1 ON my_table", 399 | "DROP SUBSCRIPTION mysub", 400 | "DROP TEXT SEARCH CONFIGURATION my_english", 401 | "DROP EVENT TRIGGER snitch", 402 | "DROP TEXT SEARCH PARSER my_parser", 403 | "DROP EXTENSION hstore", 404 | "DROP DOMAIN box", 405 | "DROP TEXT SEARCH TEMPLATE thesaurus", 406 | "DROP TRANSFORM FOR hstore LANGUAGE plpythonu", 407 | "DROP FOREIGN TABLE films, distributors", 408 | "DROP FUNCTION sqrt(int)", 409 | "DROP FUNCTION update_employee_salaries()", 410 | "DROP FUNCTION update_employee_salaries", 411 | "DROP TABLE IF EXISTS any_table CASCADE", 412 | "DROP TABLE IF EXISTS any_table", 413 | "DROP SCHEMA IF EXISTS any_schema", 414 | "DROP VIEW kinds", 415 | ]; 416 | execute_tests(tests) 417 | } 418 | 419 | #[test] 420 | fn it_correctly_converts_to_string_for_alter_obj_tests() { 421 | let tests = [ 422 | "ALTER TABLE distributors DROP CONSTRAINT distributors_pkey, ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx, ADD CONSTRAINT zipchk CHECK (char_length(zipcode) = 5), ALTER COLUMN tstamp DROP DEFAULT, ALTER COLUMN tstamp TYPE timestamp with time zone USING 'epoch'::timestamp with time zone + (date_part('epoch', tstamp) * '1 second'::interval), ALTER COLUMN tstamp SET DEFAULT now(), ALTER COLUMN tstamp DROP DEFAULT, ALTER COLUMN tstamp SET STATISTICS 5, ADD COLUMN some_int int NOT NULL, DROP COLUMN IF EXISTS other_column CASCADE", 423 | "ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address)", 424 | "ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address) NOT VALID", 425 | "ALTER TABLE a ALTER COLUMN b SET DEFAULT 1", 426 | "ALTER TABLE a ALTER COLUMN b DROP DEFAULT", 427 | "ALTER TABLE distributors RENAME CONSTRAINT zipchk TO zip_check", 428 | "ALTER TABLE distributors ADD COLUMN address varchar(30)", 429 | "ALTER TABLE distributors DROP COLUMN address", 430 | "ALTER TABLE distributors ALTER COLUMN address TYPE varchar(80), ALTER COLUMN name TYPE varchar(100)", 431 | "ALTER TABLE foo ALTER COLUMN foo_timestamp TYPE timestamp with time zone USING 'epoch'::timestamp with time zone + (foo_timestamp * '1 second'::interval)", 432 | "ALTER TABLE foo ALTER COLUMN foo_timestamp DROP DEFAULT, ALTER COLUMN foo_timestamp TYPE timestamp with time zone USING 'epoch'::timestamp with time zone + (foo_timestamp * '1 second'::interval), ALTER COLUMN foo_timestamp SET DEFAULT now()", 433 | "ALTER TABLE distributors RENAME COLUMN address TO city", 434 | "ALTER TABLE distributors RENAME TO suppliers", 435 | "ALTER TABLE distributors ALTER COLUMN street SET NOT NULL", 436 | "ALTER TABLE distributors ALTER COLUMN street DROP NOT NULL", 437 | "ALTER TABLE distributors ADD CONSTRAINT zipchk CHECK (char_length(zipcode) = 5)", 438 | "ALTER TABLE distributors DROP CONSTRAINT zipchk", 439 | "ALTER TABLE ONLY distributors DROP CONSTRAINT zipchk", 440 | "ALTER TABLE distributors ADD CONSTRAINT distfk FOREIGN KEY (address) REFERENCES addresses (address) MATCH FULL", 441 | "ALTER TABLE distributors ADD CONSTRAINT dist_id_zipcode_key UNIQUE (dist_id, zipcode)", 442 | "ALTER TABLE distributors ADD PRIMARY KEY (dist_id)", 443 | "ALTER TABLE distributors SET TABLESPACE fasttablespace", 444 | "ALTER TABLE myschema.distributors SET SCHEMA yourschema", 445 | "ALTER TABLE distributors DROP CONSTRAINT distributors_pkey, ADD CONSTRAINT distributors_pkey PRIMARY KEY USING INDEX dist_id_temp_idx", 446 | "ALTER TABLESPACE index_space RENAME TO fast_raid", 447 | "ALTER TABLESPACE x SET (seq_page_cost=3)", 448 | "ALTER TABLESPACE x RESET (random_page_cost)", 449 | "ALTER TRIGGER emp_stamp ON emp RENAME TO emp_track_chgs", 450 | "ALTER CONVERSION iso_8859_1_to_utf8 RENAME TO latin1_to_unicode", 451 | "ALTER MATERIALIZED VIEW foo RENAME TO bar", 452 | "ALTER COLLATION \"de_DE\" RENAME TO german", 453 | "ALTER TYPE electronic_mail RENAME TO email", 454 | "ALTER DOMAIN zipcode RENAME CONSTRAINT zipchk TO zip_check", 455 | "ALTER AGGREGATE myavg(int) RENAME TO my_average", 456 | "ALTER FUNCTION sqrt(int) RENAME TO square_root", 457 | "ALTER RULE notify_all ON emp RENAME TO notify_me", 458 | "ALTER VIEW foo RENAME TO bar", 459 | "ALTER FUNCTION x(y) DEPENDS ON EXTENSION a", 460 | "ALTER FUNCTION x(y) NO DEPENDS ON EXTENSION a", 461 | "ALTER PROCEDURE x(y) DEPENDS ON EXTENSION a", 462 | "ALTER ROUTINE x(y) DEPENDS ON EXTENSION a", 463 | "ALTER TRIGGER x ON y DEPENDS ON EXTENSION a", 464 | "ALTER MATERIALIZED VIEW x DEPENDS ON EXTENSION a", 465 | "ALTER SYSTEM SET fsync TO off", 466 | "ALTER SYSTEM RESET fsync", 467 | "ALTER TABLE distributors ALTER COLUMN street SET STATISTICS 5", 468 | "ALTER TABLE distributors ALTER COLUMN street SET COMPRESSION lz4", 469 | "ALTER TABLE distributors SET ACCESS METHOD name", 470 | "ALTER TABLE cities ATTACH PARTITION cities_partdef DEFAULT", 471 | "ALTER TABLE measurement DETACH PARTITION measurement_y2015m12", 472 | "ALTER TABLE measurement DETACH PARTITION measurement_y2015m12 CONCURRENTLY", 473 | "ALTER TABLE measurement DETACH PARTITION measurement_y2015m12 FINALIZE", 474 | "ALTER TABLE measurement ALTER COLUMN street_city SET EXPRESSION AS (concat(street, ' ', city))", 475 | ]; 476 | execute_tests(tests) 477 | } 478 | 479 | #[test] 480 | fn it_correctly_converts_to_string_for_index_tests() { 481 | let tests = [ 482 | "CREATE UNIQUE INDEX CONCURRENTLY dist_id_temp_idx ON distributors (dist_id)", 483 | "ALTER INDEX distributors RENAME TO suppliers", 484 | "ALTER INDEX x DEPENDS ON EXTENSION a", 485 | "DROP INDEX title_idx", 486 | ]; 487 | execute_tests(tests) 488 | } 489 | 490 | #[test] 491 | fn it_correctly_converts_to_string_for_permission_tests() { 492 | let tests = [ 493 | "GRANT select ON \"table\" TO \"user\"", 494 | "GRANT select, update, insert ON \"table\" TO \"user\"", 495 | "GRANT select ON ALL TABLES IN SCHEMA schema TO \"user\"", 496 | "GRANT select ON \"table\" TO user1, user2", 497 | "GRANT select ON \"table\" TO public", 498 | "GRANT select ON \"table\" TO CURRENT_USER", 499 | "GRANT select ON \"table\" TO CURRENT_ROLE", 500 | "GRANT select ON \"table\" TO SESSION_USER", 501 | "GRANT ALL ON \"table\" TO \"user\"", 502 | "GRANT select ON \"table\" TO \"user\" WITH GRANT OPTION", 503 | "GRANT select (\"column\") ON \"table\" TO \"user\"", 504 | "GRANT select (column1, column2) ON \"table\" TO \"user\"", 505 | "GRANT usage ON SEQUENCE sequence TO \"user\"", 506 | "GRANT usage ON ALL SEQUENCES IN SCHEMA schema TO \"user\"", 507 | "GRANT create ON DATABASE database TO \"user\"", 508 | "GRANT usage ON DOMAIN domain TO \"user\"", 509 | "GRANT usage ON FOREIGN DATA WRAPPER fdw TO \"user\"", 510 | "GRANT usage ON FOREIGN SERVER server TO \"user\"", 511 | "GRANT execute ON FUNCTION function TO \"user\"", 512 | "GRANT execute ON FUNCTION function() TO \"user\"", 513 | "GRANT execute ON FUNCTION function(string) TO \"user\"", 514 | "GRANT execute ON FUNCTION function(string, string, boolean) TO \"user\"", 515 | "GRANT execute ON ALL FUNCTIONS IN SCHEMA schema TO \"user\"", 516 | "GRANT usage ON LANGUAGE plpgsql TO \"user\"", 517 | "GRANT select ON LARGE OBJECT 1234 TO \"user\"", 518 | "GRANT create ON SCHEMA schema TO \"user\"", 519 | "GRANT create ON TABLESPACE tablespace TO \"user\"", 520 | "GRANT usage ON TYPE type TO \"user\"", 521 | "GRANT role TO \"user\"", 522 | "GRANT role1, role2 TO \"user\"", 523 | // "GRANT role TO \"user\" WITH ADMIN OPTION", // Same as ADMIN TRUE 524 | "GRANT role TO \"user\" WITH ADMIN TRUE", 525 | "GRANT role TO \"user\" WITH ADMIN FALSE", 526 | "GRANT role TO \"user\" WITH INHERIT FALSE", 527 | "GRANT role TO \"user\" WITH SET FALSE GRANTED BY user2", 528 | "DROP ROLE jonathan", 529 | "REVOKE ALL ON kinds FROM manuel", 530 | "REVOKE admins FROM joe", 531 | "REVOKE insert ON films FROM public", 532 | ]; 533 | execute_tests(tests) 534 | } 535 | 536 | #[test] 537 | fn it_correctly_converts_to_string_for_database_tests() { 538 | let tests = [ 539 | "CREATE DATABASE x OWNER abc CONNECTION LIMIT 5", 540 | "CREATE DATABASE x ENCODING \"SQL_ASCII\"", 541 | "CREATE DATABASE x LC_COLLATE \"en_US.UTF-8\"", 542 | "CREATE DATABASE x LOCATION DEFAULT", 543 | "CREATE DATABASE x TABLESPACE abc", 544 | "CREATE DATABASE x TEMPLATE TRUE", 545 | "ALTER DATABASE x CONNECTION LIMIT 5", 546 | "ALTER DATABASE x ALLOW_CONNECTIONS FALSE", 547 | "ALTER DATABASE x IS_TEMPLATE TRUE", 548 | "ALTER DATABASE x TABLESPACE abc", 549 | "ALTER DATABASE x SET work_mem TO \"10MB\"", 550 | ]; 551 | execute_tests(tests) 552 | } 553 | 554 | #[test] 555 | fn it_correctly_converts_to_string_for_extension_tests() { 556 | let tests = [ 557 | "ALTER EXTENSION x UPDATE", 558 | "ALTER EXTENSION x UPDATE TO \"1.2\"", 559 | "ALTER EXTENSION x ADD ACCESS METHOD a", 560 | "ALTER EXTENSION x DROP ACCESS METHOD a", 561 | "ALTER EXTENSION x ADD AGGREGATE a(b)", 562 | "ALTER EXTENSION x ADD CAST (a AS b)", 563 | "ALTER EXTENSION x ADD COLLATION a", 564 | "ALTER EXTENSION x ADD CONVERSION a", 565 | "ALTER EXTENSION x ADD DOMAIN a", 566 | "ALTER EXTENSION x ADD FUNCTION a(b)", 567 | "ALTER EXTENSION x ADD LANGUAGE a", 568 | "ALTER EXTENSION x ADD OPERATOR ~~(a, b)", 569 | "ALTER EXTENSION x ADD OPERATOR CLASS a USING b", 570 | "ALTER EXTENSION x ADD OPERATOR FAMILY a USING b", 571 | "ALTER EXTENSION x ADD PROCEDURE a(b)", 572 | "ALTER EXTENSION x ADD ROUTINE a(b)", 573 | "ALTER EXTENSION x ADD SCHEMA a", 574 | "ALTER EXTENSION x ADD EVENT TRIGGER a", 575 | "ALTER EXTENSION x ADD TABLE a", 576 | "ALTER EXTENSION x ADD TEXT SEARCH PARSER a", 577 | "ALTER EXTENSION x ADD TEXT SEARCH DICTIONARY a", 578 | "ALTER EXTENSION x ADD TEXT SEARCH TEMPLATE a", 579 | "ALTER EXTENSION x ADD TEXT SEARCH CONFIGURATION a", 580 | "ALTER EXTENSION x ADD SEQUENCE a", 581 | "ALTER EXTENSION x ADD VIEW a", 582 | "ALTER EXTENSION x ADD MATERIALIZED VIEW a", 583 | "ALTER EXTENSION x ADD FOREIGN TABLE a", 584 | "ALTER EXTENSION x ADD FOREIGN DATA WRAPPER a", 585 | "ALTER EXTENSION x ADD SERVER a", 586 | "ALTER EXTENSION x ADD TRANSFORM FOR a LANGUAGE b", 587 | "ALTER EXTENSION x ADD TYPE a", 588 | "CREATE EXTENSION x", 589 | "CREATE EXTENSION IF NOT EXISTS x CASCADE VERSION \"1.2\" SCHEMA a", 590 | ]; 591 | execute_tests(tests) 592 | } 593 | 594 | #[test] 595 | fn it_correctly_converts_to_string_for_multi_statements() { 596 | let tests = [ 597 | "SELECT m.name AS mname, pname FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON TRUE; INSERT INTO manufacturers_daily (a, b) SELECT a, b FROM manufacturers", 598 | "SELECT m.name AS mname, pname FROM manufacturers m LEFT JOIN LATERAL get_product_names(m.id) pname ON TRUE; UPDATE users SET name = 'bobby; drop tables'; INSERT INTO manufacturers_daily (a, b) SELECT a, b FROM manufacturers", 599 | ]; 600 | execute_tests(tests) 601 | } 602 | 603 | #[test] 604 | fn it_correctly_converts_to_string_for_xml() { 605 | let tests = [ 606 | "SELECT xmltable.* FROM xmldata, xmltable(('//ROWS/ROW') PASSING data COLUMNS id int PATH '@id', ordinality FOR ORDINALITY, \"COUNTRY_NAME\" text, country_id text PATH 'COUNTRY_ID', size_sq_km double precision PATH 'SIZE[@unit = \"sq_km\"]', size_other text PATH 'concat(SIZE[@unit!=\"sq_km\"], \" \", SIZE[@unit!=\"sq_km\"]/@unit)', premier_name text PATH 'PREMIER_NAME' DEFAULT 'not specified')", 607 | "SELECT xmlcomment('hello')", 608 | "SELECT xmlconcat('', 'foo')", 609 | "SELECT xmlconcat('', '')", 610 | "SELECT xmlelement(name foo)", 611 | "SELECT xmlelement(name foo, xmlattributes('xyz' AS bar))", 612 | "SELECT xmlelement(name foo, xmlattributes(current_date AS bar), 'cont', 'ent')", 613 | "SELECT xmlelement(name \"foo$bar\", xmlattributes('xyz' AS \"a&b\"))", 614 | "SELECT xmlelement(name test, xmlattributes(a, b)) FROM test", 615 | "SELECT xmlelement(name foo, xmlattributes('xyz' AS bar), xmlelement(name abc), xmlcomment('test'), xmlelement(name xyz))", 616 | "SELECT xmlforest('abc' AS foo, 123 AS bar)", 617 | "SELECT xmlforest(table_name, column_name) FROM information_schema.columns WHERE table_schema = 'pg_catalog'", 618 | "SELECT xmlpi(name php, 'echo \"hello world\";')", 619 | "SELECT xmlroot(xmlparse(document 'abc'), version '1.0', STANDALONE YES)", 620 | "SELECT xmlagg(x) FROM test", 621 | "SELECT xmlagg(x ORDER BY y DESC) FROM test", 622 | "SELECT xmlagg(x) FROM (SELECT * FROM test ORDER BY y DESC) tab", 623 | "SELECT xml IS DOCUMENT FROM test", 624 | "SELECT xpath('/my:a/text()', 'test', ARRAY[ARRAY['my', 'http://example.com']])", 625 | "SELECT xpath('//mydefns:b/text()', 'test', ARRAY[ARRAY['mydefns', 'http://example.com']])", 626 | ]; 627 | execute_tests(tests) 628 | } 629 | 630 | #[test] 631 | fn it_correctly_converts_to_string_for_everything_else() { 632 | let tests = [ 633 | "BEGIN", 634 | "BEGIN ISOLATION LEVEL SERIALIZABLE", 635 | "BEGIN READ ONLY", 636 | "BEGIN ISOLATION LEVEL READ COMMITTED, DEFERRABLE", 637 | "START TRANSACTION READ ONLY", 638 | "ROLLBACK", 639 | "ROLLBACK AND CHAIN", 640 | "COMMIT", 641 | "COMMIT AND CHAIN", 642 | "SAVEPOINT \"x y\"", 643 | "ROLLBACK TO SAVEPOINT x", 644 | "RELEASE x", 645 | "SET statement_timeout TO 10000", 646 | "SET search_path TO my_schema, public", 647 | "SET LOCAL search_path TO my_schema, public", 648 | "SET \"user\" TO $1", 649 | "VACUUM", 650 | "VACUUM t", 651 | "VACUUM (FULL) t", 652 | "VACUUM (FREEZE) t", 653 | "VACUUM (VERBOSE) t", 654 | "VACUUM (ANALYZE) t", 655 | "VACUUM (FULL, FREEZE, VERBOSE, ANALYZE)", 656 | "VACUUM (ANALYZE) t(a, b)", 657 | "LOCK TABLE t", 658 | "LOCK TABLE t, u", 659 | "EXPLAIN SELECT a FROM b", 660 | "EXPLAIN (ANALYZE) SELECT a FROM b", 661 | "EXPLAIN (ANALYZE, BUFFERS) SELECT a FROM b", 662 | "COPY t FROM STDIN", 663 | "COPY t(c1, c2) FROM STDIN", 664 | "COPY t FROM PROGRAM '/bin/false'", 665 | "COPY t FROM '/dev/null'", 666 | "COPY t TO STDOUT", 667 | "COPY (SELECT 1 FROM foo) TO STDOUT", 668 | "COPY t FROM STDIN WITH (convert_selectively, some_str test, some_num 1, some_list (a), some_star *)", 669 | "DO $$BEGIN PERFORM * FROM information_schema.tables; END$$", 670 | "DO LANGUAGE plpgsql $$ BEGIN PERFORM * FROM information_schema.tables; END $$", 671 | "DO $$ BEGIN PERFORM * FROM information_schema.tables; END $$ LANGUAGE plpgsql", 672 | "DISCARD ALL", 673 | "DISCARD PLANS", 674 | "DISCARD SEQUENCES", 675 | "DISCARD TEMP", 676 | "COMMENT ON POLICY a ON b IS 'test'", 677 | "COMMENT ON PROCEDURE a() IS 'test'", 678 | "COMMENT ON ROUTINE a() IS 'test'", 679 | "COMMENT ON TRANSFORM FOR int4 LANGUAGE sql IS 'test'", 680 | "COMMENT ON OPERATOR CLASS a USING b IS 'test'", 681 | "COMMENT ON OPERATOR FAMILY a USING b IS 'test'", 682 | "COMMENT ON LARGE OBJECT 42 IS 'test'", 683 | "COMMENT ON CAST (int4 AS int8) IS 'test'", 684 | "LOAD 'test file'", 685 | "COPY manual_export TO STDOUT WITH (FORMAT CSV, HEADER)", 686 | "PREPARE fooplan(int, text, bool, numeric) AS INSERT INTO foo VALUES ($1, $2, $3, $4); EXECUTE fooplan(1, 'Hunter Valley', 't', 200.00)", 687 | "PREPARE usrrptplan(int) AS SELECT * FROM users u, logs l WHERE u.usrid = $1 AND u.usrid = l.usrid AND l.date = $2; EXECUTE usrrptplan(1, current_date)", 688 | "UPDATE foo SET dataval = myval WHERE CURRENT OF curs1", 689 | ]; 690 | execute_tests(tests) 691 | } 692 | 693 | #[test] 694 | fn it_correctly_converts_to_string_for_complex_cases() { 695 | let location = Regex::new(r",\slocation:\s(-)?[\d]+").unwrap(); 696 | let tests = [ 697 | include_str!("data/sql/table_1.sql"), 698 | include_str!("data/sql/view_1.sql"), 699 | include_str!("data/sql/func_1.sql"), 700 | include_str!("data/sql/func_2.sql"), 701 | ]; 702 | 703 | // We compare the tree only for these 704 | for test in tests { 705 | let tree = match pg_parse::parse(test) { 706 | Ok(tree) => tree, 707 | Err(e) => panic!("Failed to parse: {}: \"{}\"", e, test), 708 | }; 709 | 710 | // Convert back to a string 711 | let sql = tree 712 | .iter() 713 | .map(|stmt| stmt.to_string()) 714 | .collect::>() 715 | .join("; "); 716 | 717 | // Parse it back and compare the original tree 718 | let reparsed_tree = pg_parse::parse(&sql).unwrap(); 719 | 720 | // We strip out the location from each first 721 | let original = format!("{:?}", tree); 722 | let reparsed = format!("{:?}", reparsed_tree); 723 | let original = location.replace_all(&original, "").to_string(); 724 | let reparsed = location.replace_all(&reparsed, "").to_string(); 725 | assert_eq!(original, reparsed); 726 | } 727 | } 728 | } 729 | -------------------------------------------------------------------------------- /tests/version_numbers.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_readme_deps_updated() { 3 | version_sync::assert_markdown_deps_updated!("README.md"); 4 | } 5 | 6 | #[test] 7 | fn test_html_root_url() { 8 | version_sync::assert_html_root_url_updated!("src/lib.rs"); 9 | } 10 | --------------------------------------------------------------------------------