pqi-native 0.2.0.1 → 0.2.0.2
raw patch · 4 files changed
+66/−12 lines, 4 filesdep ~pqi-conformancePVP ok
version bump matches the API change (PVP)
Dependency ranges changed: pqi-conformance
API changes (from Hackage documentation)
Files
- CHANGELOG.md +6/−0
- README.md +21/−5
- pqi-native.cabal +2/−2
- src/library/Pqi/Native/Connection.hs +37/−5
CHANGELOG.md view
@@ -1,3 +1,9 @@+# v0.2.0.2++## Fixes++- Connection startup now forwards extra conninfo params (e.g. `application_name`, `options`) instead of silently dropping everything but `user` and `database`, so they reach the server the way libpq's do. Caught by the new differential coverage in `pqi-conformance` 0.1.1.0.+ # v0.2.0.1 Documentation corrections.
README.md view
@@ -3,9 +3,15 @@ [](https://hackage.haskell.org/package/pqi-native) [](https://nikita-volkov.github.io/pqi-native/) -> **Status: Alpha.** This is a young, LLM-generated reimplementation of `libpq`'s-> wire protocol handling. It is verified against a conformance suite (see-> below), but it hasn't seen much production use yet. See+> **Status: Alpha.** `pqi-native` is an early implementation of a pure-Haskell+> transport for [`pqi`](https://github.com/nikita-volkov/pqi). It exists+> alongside [`pqi-ffi`](https://github.com/nikita-volkov/pqi-ffi), the+> established C-backed adapter, and the two are fully interchangeable: any+> code written against `pqi` runs unchanged on either one, so trying+> `pqi-native` carries no lock-in and no rewrite cost. Correctness is checked+> continuously against a conformance suite (see below), but the adapter+> hasn't yet accumulated production mileage. If you need a production-proven+> transport today, use `pqi-ffi`. See > [_Making libpq a choice_](https://nikita-volkov.github.io/pqi-making-libpq-a-choice/) > for why this project exists and what tradeoffs that implies. @@ -13,7 +19,9 @@ that speaks the PostgreSQL frontend/backend wire protocol directly — no dependency on the C `libpq` library. -`pqi-native` is an LLM-generated port of the PostgreSQL C client library, [`libpq`](https://www.postgresql.org/docs/current/libpq.html). The upstream [`libpq` source](https://github.com/postgres/postgres/tree/master/src/interfaces/libpq) is the direct reference for the implementation.+`pqi-native` reimplements the wire protocol handled by the PostgreSQL C+client library, [`libpq`](https://www.postgresql.org/docs/current/libpq.html),+from scratch in Haskell. The upstream [`libpq` source](https://github.com/postgres/postgres/tree/master/src/interfaces/libpq) is the direct reference for the implementation. ## Fidelity goal @@ -30,7 +38,15 @@ **Alpha.** The full `Pqi.Connection` capability record is implemented and verified against the `postgresql-libpq` reference via the conformance differential suite, but the library hasn't yet accumulated real-world-production mileage. Read+production mileage.++Because it implements the same `pqi` interface as+[`pqi-ffi`](https://github.com/nikita-volkov/pqi-ffi), switching between the+two is a one-line change — pass a different `Adapter` value, nothing else in+your code moves. That makes `pqi-native` low-risk to evaluate now and easy to+fall back from: adopt it where you want to shed the `libpq` dependency, and+drop back to `pqi-ffi` at any time without touching the rest of your+codebase. Read [_Making libpq a choice_](https://nikita-volkov.github.io/pqi-making-libpq-a-choice/) for the motivation and the tradeoffs of adopting it at this stage.
pqi-native.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: pqi-native-version: 0.2.0.1+version: 0.2.0.2 category: Database, PostgreSQL synopsis: Native (pure-Haskell) adapter for pqi description:@@ -146,5 +146,5 @@ build-depends: base >=4.11 && <5, hspec >=2.11 && <2.12,- pqi-conformance ^>=0.1,+ pqi-conformance ^>=0.1.1, pqi-native,
src/library/Pqi/Native/Connection.hs view
@@ -19,6 +19,7 @@ import qualified Data.ByteString as ByteString import qualified Data.ByteString.Char8 as ByteString.Char8 import qualified Data.Map.Strict as Map+import qualified Data.Set as Set import Pqi (ConnStatus (..), Notify (..), PipelineStatus (..), Verbosity (..)) import qualified Pqi.Native.Auth as Auth import Pqi.Native.Prelude@@ -36,7 +37,11 @@ port :: Int, user :: ByteString, database :: ByteString,- password :: ByteString+ password :: ByteString,+ -- | Every other recognized @key=value@ pair (e.g. @application_name@,+ -- @options@), forwarded verbatim in the startup message so the server+ -- sees them, the way libpq does.+ extraParams :: Map.Map ByteString ByteString } deriving stock (Eq, Show) @@ -71,7 +76,8 @@ port = maybe 5432 fst (ByteString.Char8.readInt (get "port" "5432")), user = theUser, database = get "dbname" theUser,- password = get "password" ""+ password = get "password" "",+ extraParams = Map.withoutKeys settings reservedKeys } where pairs = mapMaybe toPair (ByteString.Char8.words raw)@@ -83,12 +89,17 @@ | not (ByteString.null value) -> Just (key, ByteString.drop 1 value) _ -> Nothing +-- | Conninfo keys already surfaced via their own 'ConnInfo' fields, so they're+-- excluded from 'extraParams' rather than duplicated there.+reservedKeys :: Set.Set ByteString+reservedKeys = Set.fromList ["host", "port", "user", "dbname", "password"]+ -- | Parse the authority+path portion of a @postgresql://@ URI (scheme already -- stripped). Handles @[user[:password]@][host[:port]][/dbname]@; ignores query -- parameters other than what appears in those components. parseUri :: ByteString -> ByteString -> ConnInfo parseUri dfltUser withoutScheme =- ConnInfo {host, port, user, database, password}+ ConnInfo {host, port, user, database, password, extraParams} where -- Split off optional "userinfo@" prefix. The '@' is unambiguous in this -- position: hosts do not contain '@' in practice.@@ -115,6 +126,21 @@ then user else pctDecode rawDbname + -- Everything after the first '?', parsed as '&'-separated key=value pairs+ -- (e.g. ?application_name=foo&sslmode=disable).+ rawQuery = ByteString.drop 1 (ByteString.dropWhile (/= 0x3f) pathAndQuery)++ extraParams =+ Map.withoutKeys+ (Map.fromList (mapMaybe toQueryPair (ByteString.split 0x26 rawQuery)))+ reservedKeys++ toQueryPair token = case ByteString.elemIndex 0x3d token of+ Just i ->+ let (k, v) = ByteString.splitAt i token+ in if ByteString.null k then Nothing else Just (pctDecode k, pctDecode (ByteString.drop 1 v))+ Nothing -> Nothing+ -- Parse host and port from "host:port", handling IPv6 "[::1]:port". (host, port) | not (ByteString.null hostport) && ByteString.head hostport == 0x5b =@@ -241,7 +267,7 @@ pure connection Right transport -> do connection <- newConnection False transport info- sendMessage connection (startupMessage [("user", user info), ("database", database info)])+ sendMessage connection (startupMessage (startupParams info)) handshake connection pure connection @@ -268,8 +294,14 @@ writeIORef (txStatus connection) 0x49 writeIORef (connStatus connection) ConnectionBad writeIORef (lastError connection) (Just "")- sendMessage connection (startupMessage [("user", user (info connection)), ("database", database (info connection))])+ sendMessage connection (startupMessage (startupParams (info connection))) handshake connection++-- | The startup message parameter list: @user@ and @database@, plus any+-- extra conninfo params (e.g. @application_name@) forwarded verbatim.+startupParams :: ConnInfo -> [(ByteString, ByteString)]+startupParams info =+ ("user", user info) : ("database", database info) : Map.toList (extraParams info) newConnection :: Bool -> Transport -> ConnInfo -> IO Connection newConnection isNull transport info = do