Repository navigation
feat(provision): accept a unix socket host - #5
Merged
Merged
Conversation
A socket directory cannot ride in the URL authority: its slashes end the authority. libpq and pgx read the socket path from the host query parameter instead, so connect now branches on a leading slash and builds postgres:///db?host=/var/run/postgresql for that case. The TCP form is untouched. This is what the official postgres image needs. Its entrypoint runs /docker-entrypoint-initdb.d/ against a temporary server started with listen_addresses empty, reachable over the socket only, so semibase create could not run as an init script before. Extracted connectionString from connect so the construction can be tested without a server, and Endpoint so the two places that formatted host:port for a message (the connect error, the CLI's Done line) name the socket file instead when the host is a socket directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The socket endpoint printed <dir>/.s.PGSQL.<port>/<database>, which makes the socket file read as a directory and sends the operator to a path that can never exist. The database now rides in parentheses, so the socket path stays quotable. The TCP form is unchanged. The socket test claimed a leading slash was libpq's own. It is not: libpq's is_unixsock_path also takes "@" for Linux's abstract namespace, and the driver in play is pgx, whose isAbsolutePath takes a leading slash or a drive-letter path. Detection now matches pgx clause for clause. "@" stays out on purpose: pgx resolves such a host as a TCP name, so accepting it would only move the failure one layer later. TestConnectionStringIsParsedByTheDriver could not tell an encoded slash from an unencoded one - both spellings parse to the same config.Host. It is now TestConnectionStringIsDialedAsTheRightNetwork and asserts the network pgconn.NetworkAddress derives, unix for the socket form and tcp for the TCP form; the address itself is filepath.Join'd, so it differs by GOOS and is not asserted. TestIsSocketHost pins the boundary directly. Endpoint was exported only for the CLI's Done line and was handed the receiver's own field. It is now Endpoint() on the receiver over an unexported endpoint(database) for the connect error, and the Done line has a run-level test. CI's Linux job gains the step that covers the motivating path: it layers the freshly built binary onto postgres:17-alpine behind an init script and asserts semiplot_reader can select over TCP, which only opens after the init scripts have run. Nothing is published - docker-proxy binds a published host port before postgres listens. Docs: the lead no longer says pgx talks over TCP only, the --host rules move out of "Development benches" into a connection section of their own, the example URL is the percent-encoded one the tool actually emits, and the Russian operator table names the socket form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The socket step built without CGO_ENABLED=0. The runner has a C toolchain, so the default is 1 and the binary links against glibc, while postgres:17-alpine is musl. The image reports it as "not found", which reads as a missing file rather than a missing interpreter and sends a reader looking for the wrong thing. release.yml already builds the shipped Linux artifact with CGO_ENABLED=0, so only this step was wrong. A file(1) assertion follows the build, because a linking mistake should fail where it happens and say what it is, not several steps later as a shell reporting that an executable it just chmod'ed does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
semibasecould not connect over a unix socket, which blocks its intended container use.Options.connectbuilt a connection string as aurl.URLwhose authority came fromnet.JoinHostPort. A socket directory is a filesystem path, and its slashes are not valid in a URL authority —url.Parserejects the result withinvalid URL escape "%2F"before any network call happens.That matters because of how the official
postgresimage works. Its entrypoint starts a temporary server withlisten_addressesempty — no TCP at all — and runs everything in/docker-entrypoint-initdb.d/against it over the socket. Sosemibase createcould not run as an init script, which is exactly what SemiPlot's test bench needs in order to have one container provision itself before its port opens.What changed
A socket host now goes into the URL's
hostquery parameter instead of the authority:postgres://user:pass@/db?host=%2Fvar%2Frun%2Fpostgresql&port=5432. The TCP string is byte-identical to what it was — hostnames, IP literals, IPv6 in brackets, non-default ports.Socket detection matches
pgconn.isAbsolutePathclause for clause, including its quirk of requiring an upper-case Windows drive letter, so this tool and the driver never disagree about what a socket host is. The@abstract-namespace form is deliberately excluded: measured against the pinned pgx,pgconn.NetworkAddress("@abstract", 5432)returnstcp, so pgx has no abstract-socket support and routing@into the host parameter would advertise something that does not exist.The error and "Done" messages name the socket file rather than a
host:portthat was never dialled.Verification
The mechanism was proven end to end before this PR was opened, not argued:
postgres:17-alpinewith this binary in/docker-entrypoint-initdb.d/provisions over the socket.docker logsshows the temp server listening on the Unix socket with no TCP line, thencreatecompleting.semiplot_readerevery 100 ms, never once sawrole does not existordatabase does not exist. The first connection that authenticated already saw the complete result.master's binary fails withinvalid URL escape "%2F", the container exits 1, and the port never opens.--expected-majorexits 1 and the port stays shut.CI gains a step that runs that same recipe, so the motivating path is covered rather than only its string construction. The step was executed locally against Docker 29.7.2 before being written into the workflow; no GitHub Actions run has been observed yet.
TestConnectionStringIsDialedAsTheRightNetworkassertspgconn.NetworkAddressyieldsunixfor the socket form andtcpfor TCP — the property that matters, rather than a golden string copy-pasted from the implementation.Notes for the consumer
Facts measured while proving this, which SemiPlot's Dockerfile will need:
/var/run/postgresql; init scripts run aspostgres, uid 70, cwd/trust; no super password neededPGDATA, so a reused volume skips them entirelydocker-proxybinds the published host port immediately, so a raw TCP-connect readiness probe is a false positive — probe withpg_isreadyor a real query.shrather than running it, so the init script must be executable orset -eandexecinside it would hijack the entrypoint process🤖 Generated with Claude Code