summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Kreutz <mail@skreutz.com>2026-04-30 10:10:46 +0200
committerStefan Kreutz <mail@skreutz.com>2026-04-30 10:10:46 +0200
commit46a3d2ba70decd1931e13c190bfa49217e57718d (patch)
tree49bc767c52d0cb4cf8443782cae1cc641ef59343
parent47421e41def84ab92a52906f01266b1044fbfe29 (diff)
downloadtemp-postgres-46a3d2ba70decd1931e13c190bfa49217e57718d.tar.gz
Rewrite in async Rust
-rw-r--r--.gitignore3
-rw-r--r--CHANGELOG.md41
-rw-r--r--Cargo.lock1576
-rw-r--r--Cargo.toml56
-rw-r--r--LICENSE-APACHE-2.012
-rw-r--r--LICENSE-MIT19
-rw-r--r--Makefile39
-rw-r--r--README.md89
-rw-r--r--build.rs16
-rwxr-xr-xconfigure33
-rw-r--r--deny.toml22
-rw-r--r--flake.lock22
-rw-r--r--flake.nix73
-rw-r--r--justfile54
-rw-r--r--src/main.rs540
-rw-r--r--temp-postgres.198
-rwxr-xr-xtemp-postgres.sh129
-rw-r--r--tests/cli.rs356
-rw-r--r--version1
19 files changed, 2810 insertions, 369 deletions
diff --git a/.gitignore b/.gitignore
index 80a3287..fbbdb6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/README.md b/README.md
index e41fe7e..b7a7872 100644
--- a/README.md
+++ b/README.md
@@ -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 = []
diff --git a/flake.lock b/flake.lock
index d80a588..9b1f490 100644
--- a/flake.lock
+++ b/flake.lock
@@ -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"
}
diff --git a/flake.nix b/flake.nix
index 2076939..1ca32d2 100644
--- a/flake.nix
+++ b/flake.nix
@@ -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
Generated by cgit. See skreutz.com for my tech blog and contact information.