diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | CHANGELOG.md | 41 | ||||
| -rw-r--r-- | Cargo.lock | 1576 | ||||
| -rw-r--r-- | Cargo.toml | 56 | ||||
| -rw-r--r-- | LICENSE-APACHE-2.0 | 12 | ||||
| -rw-r--r-- | LICENSE-MIT | 19 | ||||
| -rw-r--r-- | Makefile | 39 | ||||
| -rw-r--r-- | README.md | 89 | ||||
| -rw-r--r-- | build.rs | 16 | ||||
| -rwxr-xr-x | configure | 33 | ||||
| -rw-r--r-- | deny.toml | 22 | ||||
| -rw-r--r-- | flake.lock | 22 | ||||
| -rw-r--r-- | flake.nix | 73 | ||||
| -rw-r--r-- | justfile | 54 | ||||
| -rw-r--r-- | src/main.rs | 540 | ||||
| -rw-r--r-- | temp-postgres.1 | 98 | ||||
| -rwxr-xr-x | temp-postgres.sh | 129 | ||||
| -rw-r--r-- | tests/cli.rs | 356 | ||||
| -rw-r--r-- | version | 1 |
19 files changed, 2810 insertions, 369 deletions
@@ -1,3 +1,2 @@ -/config.mk -/temp-postgres.1.gz /.direnv +/target diff --git a/CHANGELOG.md b/CHANGELOG.md index 03466e1..2e5dfeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,46 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep A Changelog][] and this project adheres to [Semantic Versioning][]. - -[Keep A Changelog]: https://keepachangelog.com/en/1.1.0/ -[Semantic Versioning]: https://semver.org/spec/v2.0.0.html +The format is based on [Keep A Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +This release replaces the POSIX shell script with a Rust binary crate. + +### Added + +* Added structured logging (also known as tracing), and a `--log-filter` option +* Added `-V` and `--version` options to print the short resp. long version +* Added a `--startup-timeout` option +* Added a `--shutdown-timeout` option +* Added an unwrapped package to the Nix flake +* Added an integration test for the command-line interface +* Added a development-oriented justfile +* Documented known limitations in the readme +* Explicitly stated the maintenance status in the readme + +### Changed + +* Rewrote the main POSIX shell script in async Rust +* Changed the license from ISC to MIT or APACHE-2.0 +* Changed the command-line syntax to require a double dash (`--`) before the wrapped command +* Disabled standard output and standard error of all child processes, except for the wrapped command, to enable the user to capture the output of the wrapped command +* Implemented a retry mechanism for the `pg_isready` command, see the `--startup-timeout` option +* Implemented a graceful shutdown upon `SIGINT`, see the `--shutdown-timeout` option +* Implemented the [`NO_COLOR`](https://no-color.org/) preference +* Rewrote short and long built-in help options `-h` and `--help` + +### Removed + +* Removed the mdoc manual page in favor of the improved built-in help options +* Removed the makefile + +### Fixed + +* Fixed wrapped command argument handling +* Fixed handling of `PGDATABASE` and `PGUSER` environment variables +* Fixed `SIGINT` propagation to child processes + ## [0.3.1] - 2026-04-27 ### Added diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2084bdf --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1576 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "assert_cmd" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "assert_fs" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652f6cb1f516886fcfee5e7a5c078b9ade62cfcb889524efe5a64d682dd27a9" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "anstyle", + "heck", + "proc-macro2", + "pulldown-cmark", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "color-print" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" +dependencies = [ + "color-print-proc-macro", +] + +[[package]] +name = "color-print-proc-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" +dependencies = [ + "nom", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +dependencies = [ + "bitflags", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "temp-postgres-command" +version = "0.4.0" +dependencies = [ + "anyhow", + "assert_cmd", + "assert_fs", + "clap", + "color-print", + "humantime", + "nix", + "predicates", + "tempfile", + "testresult", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", + "vergen-gitcl", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "testresult" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614b328ff036a4ef882c61570f72918f7e9c5bee1da33f8e7f91e01daee7e56c" + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8fb8dd9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,56 @@ +[package] +# Crate temp-postgres on crates.io is a library of a different project. +name = "temp-postgres-command" +version = "0.4.0" +edition = "2024" +authors = ["Stefan Kreutz <mail@skreutz.com>"] +description = "Run the PostgreSQL server off a temporary data directory" +readme = "README.md" +repository = "https://git.skreutz.com/temp-postgres.git" +license = "MIT OR Apache-2.0" +keywords = ["temporary", "postgresql", "database"] +categories = ["command-line-utilities", "database", "development-tools"] +publish = true + +[lints.rust] +unsafe_code = "forbid" +missing_docs = "warn" + +[dependencies] +anyhow = "1.0.102" +clap = { version = "4.6.1", features = [ + "derive", + "unstable-markdown", + "env", + "string", +] } +color-print = "0.3.7" +humantime = "2.3.0" +nix = { version = "0.31.2", features = ["signal"] } +tempfile = "3.27.0" +tokio = { version = "1.52.1", features = [ + "process", + "rt", + "macros", + "rt-multi-thread", + "signal", + "time", + "fs", +] } +tokio-util = "0.7.18" +tracing = "0.1.44" +tracing-subscriber = "0.3.23" +url = "2.5.8" + +[dev-dependencies] +assert_cmd = "2.2.1" +assert_fs = "1.1.3" +predicates = "3.1.4" +testresult = "0.4.1" + +[build-dependencies] +vergen-gitcl = "9.1.0" + +[[bin]] +name = "temp-postgres" +path = "src/main.rs" diff --git a/LICENSE-APACHE-2.0 b/LICENSE-APACHE-2.0 new file mode 100644 index 0000000..ae05ccb --- /dev/null +++ b/LICENSE-APACHE-2.0 @@ -0,0 +1,12 @@ +Copyright 2026 Stefan Kreutz <mail@skreutz.com> + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..b54d456 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright 2026 Stefan Kreutz <mail@skreutz.com> + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile deleted file mode 100644 index ede21d6..0000000 --- a/Makefile +++ /dev/null @@ -1,39 +0,0 @@ -# Portable makefile supporting OpenBSD, FreeBSD, and GNU/Linux. - -PREFIX = /usr/local -INSTALL = install -BINDIR = bin -BINOWN = root -BINGRP = root -BINMODE = 555 -MANDIR = man/man -MANOWN = root -MANGRP = root -MANMODE = 444 -MANGZ = 0 - -include config.mk - -all: - gzip -c temp-postgres.1 >temp-postgres.1.gz - -clean: - rm temp-postgres.1.gz - -lint: - shellcheck -a temp-postgres.sh - mandoc -T lint -W warning temp-postgres.1 - -mandoc -T lint -W all temp-postgres.1 - -install: - ${INSTALL} -o ${BINOWN} -g ${BINGRP} -m ${BINMODE} temp-postgres.sh ${DESTDIR}${PREFIX}/${BINDIR}/temp-postgres - if [ ${MANGZ} -eq 1 ] ; then \ - ${INSTALL} -o ${MANOWN} -g ${MANGRP} -m ${MANMODE} temp-postgres.1.gz ${DESTDIR}${PREFIX}/${MANDIR}1/temp-postgres.1.gz ; \ - else \ - ${INSTALL} -o ${MANOWN} -g ${MANGRP} -m ${MANMODE} temp-postgres.1 ${DESTDIR}${PREFIX}/${MANDIR}1/temp-postgres.1 ; \ - fi - -uninstall: - rm -f ${DESTDIR}${PREFIX}/${BINDIR}/temp-postgres - rm -f ${DESTDIR}${PREFIX}/${MANDIR}1/temp-postgres.1 - rm -f ${DESTDIR}${PREFIX}/${MANDIR}1/temp-postgres.1.gz @@ -1,7 +1,6 @@ # temp-postgres The `temp-postgres` utility runs the [PostgreSQL](https://www.postgresql.org/) server off a temporary data directory. -See the [original blog post](https://www.skreutz.com/posts/temporary-postgresql-server/) for an introduction. The project's source code is hosted on <https://git.skreutz.com/temp-postgres.git/>. @@ -13,7 +12,7 @@ Create and serve a temporary database: temp-postgres ``` -Wrap the `psql` command to connect to the temporary database once the server is ready: +Wrap the `psql` command for an interactive terminal: ```sh temp-postgres -- psql @@ -43,67 +42,81 @@ run-wrapper = 'temp-postgres' Set up a symlink to enable static client configuration: ```sh -temp-postgres --symlink ./db -psql --host "$(realpath ./db)" +temp-postgres --symlink db +psql --host "$(realpath db)" ``` -Specify a database name, and PostgreSQL user name: +See the built-in help for details: ```sh -temp-postgres --dbname myproject --username alex +temp-postgres --help ``` - -See the manual page for details. - ## Dependencies -The `temp-postgres` utility depends on `sh(1)` and `postgres(1)`. +The `temp-postgres` utility has a run-time dependency on [PostgreSQL](https://www.postgresql.org/). + +## Installation -On OpenBSD you can install the dependencies using `pkg_add(1)`: +You can install `temp-postgres` using Cargo: ```sh -doas pkg_add postgresql-server postgresql-client -``` +# From crates.io +cargo install temp-postgres-command -On FreeBSD you can install the dependencies using `pkg-install(8)`: +# From the hosted Git repository +cargo --config net.git-fetch-with-cli=true install --git https://git.skreutz.com/temp-postgres.git/ -```sh -sudo pkg install postgresql16-server postgresql16-client +# From your local source copy +cargo install --path . ``` -On Void Linux you can install the dependencies using `xbps-install(1)`: +Alternatively you can run or install `temp-postgres` using the experimental Nix flake: ```sh -sudo xbps-install -S dash postgresql postgresql-client -``` +# Default package +nix run git+https://git.skreutz.com/temp-postgres.git -- --help -On Arch Linux you can install the dependencies using `pacman(8)`: +# Fixed PostgreSQL major version +nix run git+https://git.skreutz.com/temp-postgres.git#temp-postgres_18 -- --help -```sh -sudo pacman -Syu postgresql +# Unwrapped package (to be used with your own PostgreSQL installation) +nix run git+https://git.skreutz.com/temp-postgres.git#temp-postgres-unwrapped -- --help ``` -## Installation +**NOTE**: Try using Nix' `--refresh` flag, or adding `?ref=main&shallow=0` to the URL if you experience caching issues. -```sh -./configure -make -sudo make install -``` +See the [NixOS example](./examples/nixos) for how to install `temp-postgres` on NixOS using Nix flakes. -Alternatively you can run or install the experimental Nix flake: +## Limitations -```sh -nix run git+https://git.skreutz.com/temp-postgres.git -- --help -``` +For now, `temp-postgres` supports only UNIX-like systems. -You can optionally select the PostgreSQL version provided by Nixpkgs: +`temp-postgres` may fail to clean up resources. +While `temp-postgres` performs a graceful shutdown on `SIGINT`, it terminates immediately when the graceful shutdown times out, and on `SIGTERM`. -```sh -nix run git+https://git.skreutz.com/temp-postgres.git#temp-postgres_18 -- --help -``` +## Maintenance -Try using Nix' `--refresh` flag, or adding `?ref=main&shallow=0` to the URL if you experience caching issues. +`temp-postgres` is semi-actively maintained. +I intend to fix bugs, and keep it working. -See the [NixOS example](./examples/nixos) for how to install `temp-postgres` on NixOS using Nix flakes. +Releases adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), see the [changelog](CHANGELOG.md). + +The minimum supported Rust version (MSRV) is the latest stable release. + +The Nix flake is experimental. + +## Contribution + +Contributions are welcome! +Please [contact](https://www.skreutz.com/contact/) me via email. + +## License + +This work is distributed under the terms of both, the [MIT License](LICENSE-MIT) and the [Apache License, Version 2.0](LICENSE-APACHE-2.0). + +## History + +The `temp-postgres` utility started as a POSIX shell script, first published in a [blog post](https://www.skreutz.com/posts/temporary-postgresql-server/) on 15 October 2020. +Six years later, in 2026, I rewrote the shell script in async Rust, partially because I wanted to improve error and signal handling, partially because why not ☺. +I chose *async* Rust because signal handling is fundamentally asynchronous. diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8fae21e --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +//! Build script. + +fn main() -> Result<(), Box<dyn std::error::Error>> { + // Embed build information. + vergen_gitcl::Emitter::new() + .idempotent() + .add_instructions( + &vergen_gitcl::GitclBuilder::default() + .sha(true) // Short + .dirty(false) // Ignore untracked files + .build()?, + )? + .emit()?; + + Ok(()) +} diff --git a/configure b/configure deleted file mode 100755 index d5df8c0..0000000 --- a/configure +++ /dev/null @@ -1,33 +0,0 @@ -#! /bin/sh - -set -eu - -exec 3>config.mk - -case "$( uname )" in - OpenBSD) - echo 'BINGRP = bin' 1>&3 - echo 'MANGRP = bin' 1>&3 - ;; - FreeBSD) - echo 'BINGRP = wheel' 1>&3 - echo 'MANDIR = share/man/man' 1>&3 - echo 'MANGRP = wheel' 1>&3 - ;; - Linux) - case "$( sed -n 's/^ID=//p' /etc/os-release )" in - void) - echo 'BINMODE = 755' 1>&3 - echo 'MANMODE = 644' 1>&3 - echo 'MANDIR = share/man/man' 1>&3 - ;; - arch) - echo 'PREFIX = /usr' 1>&3 - echo 'BINMODE = 755' 1>&3 - echo 'MANDIR = share/man/man' 1>&3 - echo 'MANMODE = 644' 1>&3 - echo 'MANGZ = 1' 1>&3 - ;; - esac - ;; -esac diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..814bc78 --- /dev/null +++ b/deny.toml @@ -0,0 +1,22 @@ +# cargo-deny configuration +# +# Default config: <https://github.com/EmbarkStudios/cargo-deny/blob/main/deny.template.toml> +# Docs: <https://embarkstudios.github.io/cargo-deny/> +# Project: <https://github.com/EmbarkStudios/cargo-deny> +# + +[licenses] +# See <https://spdx.org/licenses/> for possible licenses. +allow = ["Apache-2.0", "MIT", "Unicode-3.0"] +confidence-threshold = 1.0 + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] + +[sources.allow-org] +github = [] +gitlab = [] +bitbucket = [] @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1777335812, + "narHash": "sha256-bEg5xoAxAwsyfnGhkEX7RJViTIBIYPd8ISg4O1c0HFc=", + "owner": "ipetkov", + "repo": "crane", + "rev": "5e0fb2f64edff2822249f21293b8304dedaaf676", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-parts": { "inputs": { "nixpkgs-lib": "nixpkgs-lib" @@ -20,11 +35,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1777395829, + "narHash": "sha256-HposVFZcsBCevgqLR73w/BpSe8J1lMgw5kASnnxO3A4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "e75f25705c2934955ee5075e62530d74aca973c6", "type": "github" }, "original": { @@ -51,6 +66,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-parts": "flake-parts", "nixpkgs": "nixpkgs" } @@ -4,6 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; flake-parts.url = "github:hercules-ci/flake-parts"; + crane.url = "github:ipetkov/crane"; }; outputs = @@ -11,6 +12,7 @@ self, nixpkgs, flake-parts, + crane, ... }: flake-parts.lib.mkFlake { inherit inputs; } { @@ -28,9 +30,35 @@ ... }: let - version = builtins.head (lib.splitString "\n" (builtins.readFile ./version)); - revision = lib.toString (self.shortRev or self.dirtyShortRev or "unknown"); - mkPackage = + craneLib = crane.mkLib pkgs; + src = craneLib.cleanCargoSource ./.; + commonArgs = { + inherit src; + strictDeps = true; + }; + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + crate = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + nativeBuildInputs = [ + pkgs.coreutils + pkgs.postgresql + ]; + meta.mainProgram = "temp-postgres"; + VERGEN_GIT_SHA = + if self ? shortRev then + self.shortRev + else if self ? dirtyShortRev then + lib.strings.removeSuffix "-dirty" self.dirtyShortRev + else + "unknown"; + VERGEN_GIT_DIRTY = if self ? dirtyShortRev then "true" else "false"; + } + ); + version = crate.version; + revision = self.shortRev or self.dirtyShortRev or "unknown"; + wrap = { postgresql, suffix ? "", @@ -38,19 +66,11 @@ pkgs.stdenv.mkDerivation { name = "temp-postgres-${version}-${revision}${suffix}"; version = "${version}-${revision}"; - src = lib.cleanSource ( - builtins.path { - path = ./.; - name = "temp-postgres"; - } - ); nativeBuildInputs = [ pkgs.makeBinaryWrapper ]; - installPhase = '' + buildCommand = '' mkdir -p $out/bin - cp temp-postgres.sh $out/bin/temp-postgres - mkdir -p $out/man/man1 - cp temp-postgres.1 $out/man/man1/temp-postgres.1 - wrapProgram $out/bin/temp-postgres --prefix PATH : ${lib.makeBinPath [ postgresql ]} + makeWrapper ${crate}/bin/temp-postgres $out/bin/temp-postgres \ + --prefix PATH : ${lib.makeBinPath [ postgresql ]} ''; }; postgresqlVersions = lib.mapAttrs' ( @@ -58,7 +78,7 @@ let postgresqlVersion = lib.removePrefix "postgresql_" name; in - lib.nameValuePair "temp-postgres_${postgresqlVersion}" (mkPackage { + lib.nameValuePair "temp-postgres_${postgresqlVersion}" (wrap { postgresql = drv; suffix = "_${postgresqlVersion}"; }) @@ -67,24 +87,33 @@ { packages = { default = self'.packages.temp-postgres; - temp-postgres = mkPackage { postgresql = pkgs.postgresql; }; + temp-postgres = wrap { postgresql = pkgs.postgresql; }; + temp-postgres-unwrapped = crate; } // postgresqlVersions; - formatter = pkgs.nixfmt-tree; - devShells.default = pkgs.mkShell { nativeBuildInputs = with pkgs; [ - gnumake - gzip - mandoc + cargo + cargo-audit + cargo-deny + cargo-edit + clippy + just + nixfmt postgresql - shellcheck + rust-analyzer + rustc + rustfmt + taplo + watchexec ]; shellHook = '' export FLAKE=${self} ''; }; + + formatter = pkgs.nixfmt-tree; }; }; } diff --git a/justfile b/justfile new file mode 100644 index 0000000..31d3a8b --- /dev/null +++ b/justfile @@ -0,0 +1,54 @@ +alias fmt := format + +head := `git rev-parse HEAD` + +_default: + @just --list + +# Auto-format source code. +format: + cargo fmt --all + env RUST_LOG=warn taplo format + find . -type f -iname justfile -print0 | xargs -0rn 1 just --fmt --unstable --justfile + find . -type f -name '*.nix' -print0 | xargs -0rn 1 nixfmt + +# Run all checks. +check: + @# Check format + cargo fmt --all --check + env RUST_LOG=warn taplo format --check + find . -type f -iname justfile -print0 | xargs -0rn 1 just --fmt --unstable --check --justfile + @# nix fmt -- --ci # treefmt --ci is not read-only + find . -type f -name '*.nix' -print0 | xargs -0rn 1 nixfmt --check + @# Lint + env RUST_LOG=warn taplo lint + cargo clippy --workspace --all-targets --all-features -- --deny warnings + @# Build + cargo build --workspace --all-targets --all-features + env RUSTDOCFLAGS="-Dwarnings" cargo doc --all-features --no-deps --document-private-items + @# Test + cargo test --workspace --all-targets --all-features + @# Check dependencies for known security vulnerabilities, licenses, etc. + cargo audit + cargo deny --workspace --all-features check --show-stats bans licenses sources + @# Check Nix flake + nix flake check --print-build-logs --verbose + cd examples/nixos/ && nix flake check --print-build-logs --verbose --override-input temp-postgres ../.. + +# Update Nix flake inputs, and Rust dependencies. +update: + @# Update Nix flake inputs. + git diff --stat --exit-code + nix flake update + nix develop --command just check + git diff --stat --exit-code -- . ':(exclude)flake.lock' + git diff --no-patch --exit-code flake.lock || git commit --message 'Update Nix flake inputs' flake.lock + @# Update Rust dependencies. + git diff --stat --exit-code + nix develop --command cargo upgrade + nix develop --command cargo update + nix develop --command just check + git diff --stat --exit-code -- . ':(exclude)Cargo.toml' ':(exclude)Cargo.lock' + git diff --no-patch --exit-code Cargo.lock || git commit --message 'Update Rust dependencies' Cargo.toml Cargo.lock + @# Show commits. + git --no-pager log --stat {{ head }}.. diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..9a139ad --- /dev/null +++ b/src/main.rs @@ -0,0 +1,540 @@ +//! Run the PostgreSQL server off a temporary data directory. +//! +//! See the [supported command-line arguments](Args). + +use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, + os::unix::fs::PermissionsExt, + path::PathBuf, + process::{ExitCode, ExitStatus, Stdio}, + time::Duration, +}; + +use anyhow::{Context, bail}; +use clap::{Parser, ValueHint, builder::NonEmptyStringValueParser}; +use tokio::{process::Command, signal::unix::SignalKind, time::Instant}; +use tokio_util::sync::CancellationToken; +use tracing::{debug, info, trace, warn}; +use url::Url; + +// For clap. +macro_rules! version { + () => { + crate::version() + }; +} + +const EXTRA_HELP: &str = color_print::cstr!( + r#"<strong><underline>Exit status:</underline></strong> + +Exits 0 on success, and >0 if an error occurs. Specifically, exits 2 on usage error, and 130 when interrupted. Propagates the exit status of the given command, if any. +"# +); + +/// Command-line arguments. +#[derive(Debug, Clone, Parser)] +#[command(name = "temp-postgres", version, long_version = version!(), about, author, after_long_help = EXTRA_HELP, next_display_order = None)] +#[deny(clippy::missing_docs_in_private_items)] +struct Args { + /// Database name. + /// + /// Defaults to the PostgreSQL user name. + #[arg(short = 'd', long, env = "PGDATABASE", value_parser = NonEmptyStringValueParser::new())] + dbname: Option<String>, + + /// PostgreSQL user name. + /// + /// If not set, PostgreSQL uses the current operating system user. + #[arg(short = 'u', long, env = "PGUSER", value_parser = NonEmptyStringValueParser::new())] + username: Option<String>, + + /// Create a symbolic link to the temporary directory. + /// + /// The given path must not exist. + /// + /// The symbolic link will be created when the PostgreSQL server is ready. + /// It will be removed when the program ends if it (still) points to the temporary directory. + /// + /// Example: Static client configuration. + /// + /// ```sh + /// temp-postgres --symlink db + /// psql --host "$(realpath db)" + /// ``` + // Clap ensures that arguments passed on the command-line are non-empty. + // Add a value parser when binding to an environment variable. + #[arg(long, value_name = "PATH", value_hint = ValueHint::FilePath)] + symlink: Option<OsString>, + + /// Log level. + #[arg(long, value_enum, default_value_t = LogLevel::Info)] + log_level: LogLevel, + + /// Postgres startup timeout. + #[arg(long, value_name = "DURATION", value_parser = humantime::parse_duration, default_value = "5s")] + startup_timeout: Duration, + + /// Graceful shutdown timeout. + /// + /// `temp-postgres` performs a graceful shutdown upon SIGINT. + /// The signal is propagated to child processes, namely the `postgres` command, and the wrapped command, if any. + /// Child processes are killed (with SIGKILL) if they don't shut down in time. + #[arg(long, value_name = "DURATION", value_parser = humantime::parse_duration, default_value = "5s")] + shutdown_timeout: Duration, + + /// Command to execute once the PostgreSQL server is ready. + /// + /// The following environment variables will be passed to the command: + /// + /// - PGHOST, the absolute path to the directory in which the UNIX domain socket file is stored + /// - PGDATABASE, the database name + /// - PGUSER, the PostgreSQL user name + /// - DATABASE_URL, a connection URI + /// + /// See also the PostgreSQL documentation on environment variables and connection URIs: + /// + /// - <https://www.postgresql.org/docs/current/libpq-envars.html> + /// - <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS> + /// + /// Example: Wrap the `psql` command to connect to the temporary database once the server is ready. + /// + /// ```sh + /// temp-postgres -- psql + /// ``` + #[arg(last = true, value_hint = ValueHint::CommandWithArguments)] + command: Vec<OsString>, +} + +/// Log level. +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ValueEnum)] +enum LogLevel { + Off, + Error, + Warn, + Info, + Debug, + Trace, +} + +impl From<LogLevel> for tracing_subscriber::filter::LevelFilter { + fn from(value: LogLevel) -> Self { + match value { + LogLevel::Off => tracing_subscriber::filter::LevelFilter::OFF, + LogLevel::Error => tracing_subscriber::filter::LevelFilter::ERROR, + LogLevel::Warn => tracing_subscriber::filter::LevelFilter::WARN, + LogLevel::Info => tracing_subscriber::filter::LevelFilter::INFO, + LogLevel::Debug => tracing_subscriber::filter::LevelFilter::DEBUG, + LogLevel::Trace => tracing_subscriber::filter::LevelFilter::TRACE, + } + } +} + +#[tokio::main] +async fn main() -> ExitCode { + let args = Args::parse(); // May exit 0 or 2. Respects `NO_COLOR`. + let result = async { + let mut subscriber = tracing_subscriber::fmt() + .with_max_level(args.log_level.to_owned()) + .with_writer(std::io::stderr); // Enable user to capture standard output of wrapped command. + if std::env::var_os("NO_COLOR").is_some() { + subscriber = subscriber.with_ansi(false); + } + subscriber.init(); + debug!(version = version(), "starting"); + debug!(arguments = ?args, "parsed command-line arguments"); + + run(args).await + }; + match result.await { + Ok(_) => ExitCode::SUCCESS, + Err(err) => { + // Relying on tracing to print error. + //eprintln!("Error: {err:?}"); + if err.is::<InterruptError>() { + ExitCode::from(130) + } else if let Some(err) = err.downcast_ref::<WrappedCommandError>() { + match err.0.code().and_then(|code| u8::try_from(code).ok()) { + Some(code) => ExitCode::from(code), + None => ExitCode::FAILURE, + } + } else { + ExitCode::FAILURE + } + } + } +} + +#[tracing::instrument(level = "info", skip_all, err(Debug))] +async fn run(args: Args) -> Result<(), anyhow::Error> { + let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt()) + .context("Failed to register SIGINT handler")?; + debug!("registered SIGINT handler"); + + let tmp = tempfile::Builder::new() + .prefix("temp-postgres-") + .permissions(std::fs::Permissions::from_mode(0o700)) + .tempdir() + .context("Failed to create temporary directory")?; + debug!(directory = %&tmp.path().display(), "created temporary directory"); + + let token = CancellationToken::new(); + let mut server = tokio::spawn(serve(args.clone(), tmp.path().to_path_buf(), token.clone())); + debug!("spawned server task"); + + let result = tokio::select! { + _ = sigint.recv() => { + info!("received SIGINT, shutting down ..."); + token.cancel(); + tokio::select! { + _ = tokio::time::sleep(args.shutdown_timeout) => { + warn!("graceful shutdown timed out, aborting ..."); + server.abort(); + server.await? + } + r = &mut server => { + debug!("server task finished"); + r? + } + } + } + r = &mut server => { + debug!("server task finished"); + r? + }, + }; + + if let Some(symlink) = &args.symlink { + if let Ok(target) = tokio::fs::canonicalize(&symlink).await + && let Ok(tmp) = tokio::fs::canonicalize(&tmp.path()).await + && target == tmp + { + match tokio::fs::remove_file(symlink).await { + Ok(()) => debug!(?symlink, "removed symlink"), + Err(err) => warn!(?symlink, error = ?err, "failed to remove symlink"), + } + } else { + warn!(?symlink, directory = %&tmp.path().display(), "keeping symlink because it doesn't point to temporary directory"); + } + } + + let tmp_dir = tmp.path().to_path_buf(); + match tmp.close() { + Ok(()) => debug!(directory = %tmp_dir.display(), "removed temporary directory"), + Err(err) => { + warn!(directory = %tmp_dir.display(), error = ?err, "failed to remove temporary directory") + } + } + result +} + +#[tracing::instrument(level = "info", skip_all, err)] +async fn serve( + args: Args, + tmp_dir: PathBuf, + token: CancellationToken, +) -> Result<(), anyhow::Error> { + // WORKAROUND: PostgreSQL expects a non-empty dbname when username is set. + let args = Args { + dbname: args.dbname.clone().or(args.username.clone()), + ..args + }; + + // TODO: Check token.is_cancelled() before every command? + let mut optional_initdb_args = Vec::<&OsStr>::new(); + if let Some(ref username) = args.username { + optional_initdb_args.push("--username".as_ref()); + optional_initdb_args.push(username.as_ref()); + } + let status = Command::new("initdb") + .arg("--pgdata") + .arg(&tmp_dir) + .args(optional_initdb_args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) // In case this task gets aborted + .status() + .await + .context("Failed to spawn or wait for initdb command")?; + if !status.success() { + bail!("initdb command exited error: {status}"); + } + debug!("initialized database"); + + let mut postgres = Command::new("postgres") + .arg("-h") + .arg("") // Don't bind to a TCP/IP address + .arg("-k") + .arg(&tmp_dir) + .arg("-D") + .arg(&tmp_dir) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn postgres command")?; + debug!("spawned postgres command"); + + let now = Instant::now(); + tokio::time::sleep(Duration::from_millis(50)).await; + for i in 1.. { + match is_ready(tmp_dir.as_ref(), args.username.as_deref()).await { + Ok(status) => { + if status.success() { + debug!("PostgreSQL server is ready"); + break; + } else if status.code() == Some(1) || status.code() == Some(2) { + if now.elapsed() < args.startup_timeout { + trace!("retrying pg_isready command ..."); + } else { + bail!("pg_isready timed out after {i} attempts"); + } + } else { + bail!("pg_isready command exited error: {status}"); + } + } + Err(err) => return Err(err), + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + + let mut optional_createdb_args = Vec::<&OsStr>::new(); + if let Some(ref username) = args.username { + optional_createdb_args.push("--username".as_ref()); + optional_createdb_args.push(username.as_ref()); + } + if let Some(ref dbname) = args.dbname { + // Positional argument + optional_createdb_args.push(dbname.as_ref()); + } + let status = Command::new("createdb") + .arg("--host") + .arg(&tmp_dir) + .arg("--no-password") + .args(optional_createdb_args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .status() + .await + .context("Failed to spawn or wait for createdb command")?; + if !status.success() { + bail!("createdb command exited error: {status}"); + } + debug!("Created PostgreSQL database"); + + let mut url = Url::parse("postgresql://")?; + if let Some(ref dbname) = args.dbname { + url.set_path(dbname); + } + url.query_pairs_mut() + .append_pair("host", &tmp_dir.display().to_string()); + if let Some(ref username) = args.username { + url.query_pairs_mut().append_pair("user", username); + } + + info!( + PGHOST = %&tmp_dir.display(), + PGDATABASE = &args.dbname, + PGUSER = &args.username, + DATABASE_URL = %&url, + "You can connect using the following parameters" + ); + + if let Some(symlink) = &args.symlink { + tokio::fs::symlink(&tmp_dir, symlink) + .await + .context("Failed to create symlink")?; + debug!(?symlink, "created symlink"); + } + + let mut wrapped_command = match args.command.split_first() { + None => None, + Some((head, tail)) => { + let mut optional_env = HashMap::new(); + if let Some(ref username) = args.username { + optional_env.insert("PGUSER", username); + } + if let Some(ref dbname) = args.dbname { + optional_env.insert("PGDATABASE", dbname); + } + let command = Command::new(head) + .args(tail) + .env("PGHOST", &tmp_dir) + .env("DATABASE_URL", url.as_str()) // for SQLx with SQLX_OFFLINE=true + .envs(optional_env) + .kill_on_drop(true) + .spawn() + .context("Failed to spawn wrapped command")?; + debug!("spawned wrapped command"); + Some(command) + } + }; + + tokio::select! { + _ = token.cancelled() => { + debug!("token cancelled"); + debug!("shutting down child processes ..."); + // Propagating SIGINT to child processes in case it was send to temp-postgres only. + // Halving shutdown timeout to avoid being aborted by parent task. + let shutdown_timeout = args.shutdown_timeout.checked_div(2).unwrap(); + let (wrapped_command_result, postgres_result) = tokio::join!( + async { + if let Some(ref mut c) = wrapped_command { + interrupt_or_kill(c, shutdown_timeout).await + } else { + Ok(None) + } + }, + interrupt_or_kill(&mut postgres, shutdown_timeout), + ); + if wrapped_command.is_some() { + match wrapped_command_result { + // Exit code is probably 130 + Ok(status) => debug!(?status, "successfully shut down wrapped command"), + Err(err) => warn!(error = ?err, "failed to shut down wrapped command"), + } + } + match postgres_result { + // Exit code is probably 130 + Ok(status) => debug!(?status, "successfully shut down postgres command"), + Err(err) => warn!(error = ?err, "failed to shut down postgres command"), + } + Err(InterruptError{})? + }, + r = postgres.wait() => { + debug!("postgres command finished"); + debug!("shutting down wrapped command ..."); + if let Some(ref mut c) = wrapped_command { + match interrupt_or_kill(c, args.shutdown_timeout).await { + // Exit code is probably 130 + Ok(status) => debug!(?status, "successfully shut down wrapped command"), + Err(err) => warn!(error = ?err, "failed to shut down wrapped command"), + } + } + let status = r.context("failed to wait for postgres command")?; + if !status.success() { + bail!("postgres command exited error: {status}"); + } + Ok(()) + }, + r = conditional_wait(&mut wrapped_command) => { + debug!("wrapped command finished"); + debug!("shutting down postgres command ..."); + match interrupt_or_kill(&mut postgres, args.shutdown_timeout).await { + // Exit code is probably 130 + Ok(status) => debug!(?status, "successfully shut down postgres command"), + Err(err) => warn!(error = ?err, "failed to shut down postgres command"), + } + let status = r.context("failed to wait for wrapped command")?; + if !status.success() { + return Err(WrappedCommandError(status))?; + } + Ok(()) + }, + } +} + +fn interrupt(child: &tokio::process::Child) -> Result<(), anyhow::Error> { + // std::os::unix::process::ChildExt::send_signal is nightly-only experimental + if let Some(id) = child.id() { + nix::sys::signal::kill( + nix::unistd::Pid::from_raw(id as i32), + nix::sys::signal::Signal::SIGINT, + ) + .context("Failed to send SIGINT to process {id}")?; + } + Ok(()) +} + +async fn interrupt_or_kill( + child: &mut tokio::process::Child, + timeout: Duration, +) -> Result<Option<ExitStatus>, anyhow::Error> { + interrupt(child)?; + tokio::select! { + r = child.wait() => { + r.map(Some).context("Failed to wait for child process") + }, + _ = tokio::time::sleep(timeout) => { + child.kill().await.context("Failed to kill child process")?; + Ok(None) + } + } +} + +async fn is_ready(tmp_dir: &OsStr, username: Option<&str>) -> Result<ExitStatus, anyhow::Error> { + let mut optional_isready_args = Vec::<&OsStr>::new(); + if let Some(ref username) = username { + optional_isready_args.push("--username".as_ref()); + optional_isready_args.push(username.as_ref()); + } + Command::new("pg_isready") + .arg("--host") + .arg(tmp_dir) + .arg("--dbname") + .arg("dummy") // No database created yet + .arg("--timeout") + .arg("3") // Default + .args(optional_isready_args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .kill_on_drop(true) + .status() + .await + .context("Failed to spawn or wait for pg_isready command") +} + +async fn conditional_wait( + child: &mut Option<tokio::process::Child>, +) -> Result<std::process::ExitStatus, std::io::Error> { + match child { + None => std::future::pending().await, + Some(child) => child.wait().await, + } +} + +#[derive(Debug)] +struct WrappedCommandError(ExitStatus); + +impl std::fmt::Display for WrappedCommandError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Wrapped command exited error: {}", self.0) + } +} + +impl std::error::Error for WrappedCommandError {} + +#[derive(Debug)] +struct InterruptError {} + +impl std::fmt::Display for InterruptError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Interrupted") + } +} + +impl std::error::Error for InterruptError {} + +fn version() -> String { + // https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates + let package_version = option_env!("CARGO_PKG_VERSION").unwrap_or("unknown"); + let revision = option_env!("VERGEN_GIT_SHA").unwrap_or("unknown"); + let dirty = option_env!("VERGEN_GIT_DIRTY").and_then(|s| s.trim().parse::<bool>().ok()); + + let mut version = format!("{package_version}-{revision}"); + if let Some(true) = dirty { + version.push_str("-dirty"); + } + version +} + +#[test] +fn cli() { + use clap::CommandFactory; + Args::command().debug_assert(); +} diff --git a/temp-postgres.1 b/temp-postgres.1 deleted file mode 100644 index c3e395d..0000000 --- a/temp-postgres.1 +++ /dev/null @@ -1,98 +0,0 @@ -.\" Copyright (c) 2022, 2026 Stefan Kreutz <mail@skreutz.com> -.\" -.\" Permission to use, copy, modify, and distribute this software for any -.\" purpose with or without fee is hereby granted, provided that the above -.\" copyright notice and this permission notice appear in all copies. -.\" -.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -.Dd $Mdocdate: April 15 2026 $ -.Dt TEMP-POSTGRES 1 -.Os -.Sh NAME -.Nm temp-postgres -.Nd temporary PostgreSQL server -.Sh SYNOPSIS -.Nm temp-postgres -.Op options -.Op Fl - -.Op Ar command -.Sh DESCRIPTION -The -.Nm -utility runs the PostgreSQL server off a temporary data directory. -.Pp -The options are as follows: -.Bl -tag -width Ds -.It Fl h , -help -Print help. -.It Fl d , -dbname Ar dbname -Database name. -Defaults to the name of the effective user. -.It Fl u , -username Ar username -PostgreSQL user name. -Defaults to the name of the effective user. -.It Fl -symlink Ar path -Create a symlink to the directory in which the UNIX domain socket file is stored. -.El -.Pp -.Nm -can optionally execute a given -.Ar command -once the PostgreSQL server is ready. -If so, -.Nm -will pass the following environment variables to the -.Ar command : -.Bl -tag -width Ds -.It Ev PGHOST -Absolute path to the directory in which the UNIX domain socket file is stored. -.It Ev PGDATABASE -Database name. -.It Ev PGUSER -PostgreSQL user name. -.It Ev DATABASE_URI -Connection URI. -.It Ev DATABASE_URL -Connection URI. -.El -.Pp -See also the -.Lk https://www.postgresql.org/docs/current/libpq-envars.html "environment variables used by libpg" , -and the -.Lk https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS "connection URI scheme understood by libpg" . -.Sh EXIT STATUS -.Ex -std temp-postgres -.Sh EXAMPLES -Create and serve a temporary database: -.Bd -literal -offset indent -temp-postgres -.Ed -.Pp -Wrap the -.Xr psql 1 -command to connect to the temporary database once the server is ready: -.Bd -literal -offset indent -temp-postgres -- psql -.Ed -.Pp -Set up a symlink to enable static client configuration: -.Bd -literal -offset indent -temp-postgres --symlink ./db -psql --host "$(realpath ./db)" -.Ed -.Pp -Specify a database name, and PostgreSQL user name: -.Bd -literal -offset indent -temp-postgres --dbname myproject --username alex -.Ed -.Sh SEE ALSO -.Xr postgres 1 , -.Xr psql 1 -.Sh AUTHORS -.An Stefan Kreutz Aq Mt mail@skreutz.com diff --git a/temp-postgres.sh b/temp-postgres.sh deleted file mode 100755 index e9a901c..0000000 --- a/temp-postgres.sh +++ /dev/null @@ -1,129 +0,0 @@ -#! /bin/sh - -# Copyright (c) 2019, 2020, 2022, 2026 Stefan Kreutz <mail@skreutz.com> -# -# Permission to use, copy, modify, and distribute this software for any purpose -# with or without fee is hereby granted, provided that the above copyright -# notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR -# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. - -set -o nounset - -# Remove the temporary directory before exiting. -trap 'quit' INT -quit() { - code="${1:-0}" - [ -z "$postgres_pid" ] || kill -TERM "$postgres_pid" - wait - rm -rf "${tmpdir-}" || { - ( >&2 printf "temp-postgres: failed to remove temporary directory: %s\\n" "${tmpdir}" ) - [ "${code}" -ne 0 ] || code=1 - } - [ -z "${symlink:-}" ] || ! [ -L "$symlink" ] || unlink "$symlink" || { - ( >&2 printf "temp-postgres: failed to unlink %s\\n" "$symlink" ) - [ "${code}" -ne 0 ] || code=1 - } - exit "${code}" -} - -dbname="$( id -un )" -username="$( id -un )" - -while [ $# -gt 0 ] ; do - case "$1" in - -h|--help) - echo "Usage: temp-postgres [-h|--help] [-d|--dbname <dbname>] [-u|--username <username>] [--symlink <path>] [--] [<command>]" - exit - ;; - -d|--dbname) - dbname="$2" - shift 2 - ;; - -u|--username) - username="$2" - shift 2 - ;; - --symlink) - symlink="$2" - shift 2 - ;; - --) - shift - break - ;; - -*) - ( >&2 echo "undefined option: $1" ) - exit 2 - ;; - *) - break - ;; - esac -done - -# Create a temporary directory -tmpdir="$( mktemp -d )" || { - ( >&2 printf "temp-postgres: failed to create temporary directory\\n" ) - quit 1 -} - -# Initialize the directory -initdb --pgdata="${tmpdir}" --username="${username}" || { - ( >&2 printf "temp-postgres: failed to initialize database\\n" ) - quit 1 -} - -# Serve the directory -( postgres -h "" -k "${tmpdir}" -D "${tmpdir}" </dev/null ) & -postgres_pid=$! - -# Test the connection -sleep 1 -pg_isready --host="${tmpdir}" --dbname="nope" --username="${username}" --timeout=10 || { - ( >&2 printf "temp-postgres: failed to connect to server\\n" ) - quit 1 -} - -# Create the database -createdb --host="${tmpdir}" --username="${username}" --no-password "${dbname}" || { - ( >&2 printf "temp-postgres: failed to create database\\n" ) - quit 1 -} - -if [ -n "${symlink:-}" ] ; then - ln -sf "${tmpdir}" "$symlink" || { - ( >&2 printf "temp-postgres: failed to create symlink %s\\n" "$symlink" ) - quit 1 - } -fi - -printf ' -Connect with the following command: - -\tpsql --host "%s" --dbname "%s" --username "%s" - -' "$tmpdir" "$dbname" "$username" - -if [ $# -eq 0 ] ; then - wait -else - export PGHOST="$tmpdir" - export PGDATABASE="$dbname" - export PGUSER="$username" - export DATABASE_URI="postgresql:${dbname}?host=${tmpdir}&user=${username}" - export DATABASE_URL="$DATABASE_URI" - - "$@" || { - code=$? - ( >&2 echo "temp-postgres: command exited ${code}" ) - quit "$code" - } - quit -fi diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..0b99f8b --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,356 @@ +//! Command-line interface integration test. + +use std::{ + process::{Child, Stdio}, + time::Duration, +}; + +use assert_cmd::{Command, prelude::*}; +use assert_fs::TempDir; +use predicates::prelude::*; +use testresult::TestResult; + +#[test] +fn short_help() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("-h") + .assert() + .success() + .stdout(predicate::str::contains("Usage")) + .stderr(predicate::str::is_empty()); + Ok(()) +} + +#[test] +fn long_help() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("Usage")) + .stderr(predicate::str::is_empty()); + Ok(()) +} + +#[test] +fn short_version() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("-V") + .assert() + .success() + .stdout(predicate::str::is_match( + r#"^temp-postgres [0-9]+\.[0-9]+\.[0-9]+\n$"#, + )?) + .stderr(predicate::str::is_empty()); + Ok(()) +} + +#[test] +fn long_version() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--version") + .assert() + .success() + .stdout(predicate::str::is_match( + r#"^temp-postgres [0-9]+\.[0-9]+\.[0-9]+-[0-9a-f]{7}(-dirty)?\n$"#, + )?) + .stderr(predicate::str::is_empty()); + Ok(()) +} + +#[test] +fn wrapped_true() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--") + .arg("true") + .assert() + .success() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("PGHOST")); + Ok(()) +} + +#[test] +fn wrapped_false() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--") + .arg("false") + .assert() + .failure() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("Wrapped command exited error")); + Ok(()) +} + +#[test] +fn log_level_off() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--log-level") + .arg("off") + .arg("--") + .arg("true") + .assert() + .success() + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::is_empty()); + Ok(()) +} + +#[test] +fn no_such_flag() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--foo") + .assert() + .code(2) + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("unexpected argument")); + Ok(()) +} + +#[test] +fn unexpected_argument() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("foo") + .assert() + .code(2) + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("unexpected argument")); + Ok(()) +} + +#[test] +fn missing_username_argument() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--username") + .assert() + .code(2) + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("--username")); + Ok(()) +} + +#[test] +fn empty_username_argument() -> TestResult { + Command::cargo_bin("temp-postgres")? + .arg("--username") + .arg("") + .assert() + .code(2) + .stdout(predicate::str::is_empty()) + .stderr(predicate::str::contains("--username")); + Ok(()) +} + +#[test] +fn symlink() -> TestResult { + let tmp_dir = TempDir::new()?; + let symlink = tmp_dir.join("db"); + let child = std::process::Command::cargo_bin("temp-postgres")? + .arg("--username") + .arg("alex") + .arg("--dbname") + .arg("myproject") + .arg("--symlink") + .arg(&symlink) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + for i in (0..100).rev() { + std::thread::sleep(Duration::from_millis(200)); + if symlink.exists() { + break; + } else if i > 0 { + continue; + } else { + panic!("symlink not created in time"); + } + } + Command::new("psql") + .arg("--no-psqlrc") + .arg("--no-align") + .arg("--tuples-only") + .arg("--host") + .arg(&symlink) + .arg("--dbname") + .arg("myproject") + .arg("--username") + .arg("alex") + .write_stdin("SELECT current_database(), usename FROM pg_user;") + .assert() + .success() + .stdout(predicate::str::is_match(r#"^myproject|alex\n$"#)?) + .stderr(predicate::str::is_empty()); + interrupt(&child); + child.wait_with_output()?.assert().code(130); + Ok(()) +} + +#[test] +fn interrupted() -> TestResult { + let tmp_dir = TempDir::new()?; + let symlink = tmp_dir.join("db"); + let child = std::process::Command::cargo_bin("temp-postgres")? + .arg("--symlink") + .arg(&symlink) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + for i in (0..100).rev() { + std::thread::sleep(Duration::from_millis(200)); + if symlink.exists() { + break; + } else if i > 0 { + continue; + } else { + panic!("symlink not created in time"); + } + } + interrupt(&child); + child.wait_with_output()?.assert().code(130); + Ok(()) +} + +#[test] +fn interrupted_empty_command() -> TestResult { + let tmp_dir = TempDir::new()?; + let symlink = tmp_dir.join("db"); + let child = std::process::Command::cargo_bin("temp-postgres")? + .arg("--symlink") + .arg(&symlink) + .arg("--") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + for i in (0..100).rev() { + std::thread::sleep(Duration::from_millis(200)); + if symlink.exists() { + break; + } else if i > 0 { + continue; + } else { + panic!("symlink not created in time"); + } + } + interrupt(&child); + child.wait_with_output()?.assert().code(130); + Ok(()) +} + +#[test] +fn interrupted_sleep_command() -> TestResult { + let tmp_dir = TempDir::new()?; + let symlink = tmp_dir.join("db"); + let child = std::process::Command::cargo_bin("temp-postgres")? + .arg("--symlink") + .arg(&symlink) + .arg("--") + .arg("sleep") + .arg("30") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn()?; + for i in (0..100).rev() { + std::thread::sleep(Duration::from_millis(200)); + if symlink.exists() { + break; + } else if i > 0 { + continue; + } else { + panic!("symlink not created in time"); + } + } + interrupt(&child); + child.wait_with_output()?.assert().code(130); + Ok(()) +} + +#[test] +fn wrapped_cat() -> TestResult { + assert_cmd::Command::cargo_bin("temp-postgres")? + .arg("--") + .arg("cat") + .write_stdin("foo") + .assert() + .success() + .stdout(predicate::str::is_match(r#"^foo$"#)?) + .stderr(predicate::str::contains("PGHOST")); + Ok(()) +} + +#[test] +fn wrapped_psql_with_username() -> TestResult { + assert_cmd::Command::cargo_bin("temp-postgres")? + .arg("--username") + .arg("alex") + .arg("--") + .arg("psql") + .arg("--no-psqlrc") + .arg("--no-align") + .arg("--tuples-only") + .write_stdin("SELECT current_database(), usename FROM pg_user;") + .assert() + .success() + .stdout(predicate::str::is_match(r#"^alex|alex\n$"#)?) + .stderr(predicate::str::contains("PGHOST")); + Ok(()) +} + +#[test] +fn wrapped_psql_with_username_and_dbname() -> TestResult { + assert_cmd::Command::cargo_bin("temp-postgres")? + .arg("--username") + .arg("alex") + .arg("--dbname") + .arg("myproject") + .arg("--") + .arg("psql") + .arg("--no-psqlrc") + .arg("--no-align") + .arg("--tuples-only") + .write_stdin("SELECT current_database(), usename FROM pg_user;") + .assert() + .success() + .stdout(predicate::str::is_match(r#"^myproject|alex\n$"#)?) + .stderr(predicate::str::contains("PGHOST")); + Ok(()) +} + +#[test] +fn wrapped_psql_with_database_url() -> TestResult { + assert_cmd::Command::cargo_bin("temp-postgres")? + .arg("--username") + .arg("alex") + .arg("--dbname") + .arg("myproject") + .arg("--") + .arg("env") + .arg("--unset") + .arg("PGHOST") + .arg("--unset") + .arg("PGDATABASE") + .arg("--unset") + .arg("PGUSER") + .arg("--") + .arg("bash") + .arg("-c") + .arg("psql --no-psqlrc --no-align --tuples-only \"$DATABASE_URL\"") + .write_stdin("SELECT current_database(), usename FROM pg_user;") + .assert() + .success() + .stdout(predicate::str::is_match(r#"^myproject|alex\n$"#)?) + .stderr(predicate::str::contains("PGHOST")); + Ok(()) +} + +fn interrupt(child: &Child) { + // std::os::unix::process::ChildExt::send_signal is nightly-only experimental + nix::sys::signal::kill( + nix::unistd::Pid::from_raw(child.id() as i32), + nix::sys::signal::Signal::SIGINT, + ) + .unwrap(); +} diff --git a/version b/version deleted file mode 100644 index 9e11b32..0000000 --- a/version +++ /dev/null @@ -1 +0,0 @@ -0.3.1 |