From 918591d9adf732b5f0d6ecaa67b93f54f4d64fda Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Wed, 12 Aug 2026 15:37:57 -0700 Subject: [PATCH 1/7] feat: haskell acceptance tests --- .github/workflows/ci.yml | 16 +++++ integration/haskell/.gitignore | 1 + integration/haskell/cabal.project | 3 + .../haskell/pgdog-haskell-integration.cabal | 18 +++++ integration/haskell/run.sh | 21 ++++++ integration/haskell/test/Main.hs | 69 +++++++++++++++++++ 6 files changed, 128 insertions(+) create mode 100644 integration/haskell/.gitignore create mode 100644 integration/haskell/cabal.project create mode 100644 integration/haskell/pgdog-haskell-integration.cabal create mode 100755 integration/haskell/run.sh create mode 100644 integration/haskell/test/Main.hs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9447479c..6551b78fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,6 +77,7 @@ jobs: - { name: ruby, script: integration/ruby/run.sh } - { name: java, script: integration/java/run.sh } - { name: elixir, script: integration/elixir/run.sh, needs_beam: true } + - { name: haskell, script: integration/haskell/run.sh, needs_haskell: true } - { name: mirror, script: integration/mirror/run.sh } - { name: sql, script: integration/sql/run.sh } - { name: toxi, script: integration/toxi/run.sh } @@ -135,6 +136,21 @@ jobs: with: otp-version: "28" elixir-version: "1.20.3" + - name: Install GHC/Cabal + if: matrix.needs_haskell + id: setup-haskell + uses: haskell-actions/setup@v2 + with: + ghc-version: "9.12.4" + cabal-version: "latest" + - name: Cache Cabal dependencies + if: matrix.needs_haskell + uses: actions/cache@v5 + with: + path: ${{ steps.setup-haskell.outputs.cabal-store }} + key: cabal-${{ runner.os }}-${{ steps.setup-haskell.outputs.ghc-version }}-${{ hashFiles('integration/haskell/*.cabal', 'integration/haskell/cabal.project', 'integration/haskell/**/*.hs') }} + restore-keys: | + cabal-${{ runner.os }}-${{ steps.setup-haskell.outputs.ghc-version }}- - name: Setup dependencies run: bash integration/ci/setup.sh --with-toxi - name: Run ${{ matrix.name }} diff --git a/integration/haskell/.gitignore b/integration/haskell/.gitignore new file mode 100644 index 000000000..c33954f53 --- /dev/null +++ b/integration/haskell/.gitignore @@ -0,0 +1 @@ +dist-newstyle/ diff --git a/integration/haskell/cabal.project b/integration/haskell/cabal.project new file mode 100644 index 000000000..9de1efe63 --- /dev/null +++ b/integration/haskell/cabal.project @@ -0,0 +1,3 @@ +packages: . + +tests: true diff --git a/integration/haskell/pgdog-haskell-integration.cabal b/integration/haskell/pgdog-haskell-integration.cabal new file mode 100644 index 000000000..6a6a12201 --- /dev/null +++ b/integration/haskell/pgdog-haskell-integration.cabal @@ -0,0 +1,18 @@ +cabal-version: 3.0 +name: pgdog-haskell-integration +version: 0.1.0.0 +build-type: Simple + +test-suite persistent-integration + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: test + default-language: GHC2021 + ghc-options: -Wall + build-depends: + base >=4.18 && <5, + hspec >=2.11 && <3, + monad-logger >=0.3 && <0.4, + persistent >=2.18.1 && <2.19, + persistent-postgresql >=2.14.3 && <2.15, + text >=1.2 && <3 diff --git a/integration/haskell/run.sh b/integration/haskell/run.sh new file mode 100755 index 000000000..f2acea807 --- /dev/null +++ b/integration/haskell/run.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +source "${SCRIPT_DIR}/../common.sh" + +bash "${SCRIPT_DIR}/../ci/apt.sh" libpq-dev pkg-config + +if ! command -v cabal >/dev/null 2>&1; then + echo "cabal not found. Install GHC and Cabal, then re-run." >&2 + exit 1 +fi + +run_pgdog +wait_for_pgdog + +pushd "${SCRIPT_DIR}" +cabal test --test-show-details=direct +popd + +stop_pgdog diff --git a/integration/haskell/test/Main.hs b/integration/haskell/test/Main.hs new file mode 100644 index 000000000..3a46b6a0d --- /dev/null +++ b/integration/haskell/test/Main.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE UndecidableInstances #-} + +module Main (main) where + +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Logger (runNoLoggingT) +import Data.Text (Text) +import Database.Persist +import Database.Persist.Postgresql +import Database.Persist.TH +import Test.Hspec + +share + [mkPersist sqlSettings, mkMigrate "migrateAll"] + [persistLowerCase| +PersistentThing sql=haskell_persistent + value Text + deriving Eq Show +|] + +connectionString :: ConnectionString +connectionString = "host=127.0.0.1 port=6432 user=pgdog password=pgdog dbname=pgdog sslmode=disable" + +main :: IO () +main = runNoLoggingT $ + withPostgresqlPool connectionString 2 $ \pool -> + liftIO $ do + runSqlPool (runMigration migrateAll) pool + hspec $ + before_ (runSqlPool clearTestRows pool) $ + describe "Persistent through PgDog" $ do + it "connects and runs a typed query" $ do + result <- runSqlPool selectOne pool + result `shouldBe` [Single 1] + + it "creates, reads, updates, and deletes a row" $ + runSqlPool basicCrud pool + +clearTestRows :: SqlPersistT IO () +clearTestRows = deleteWhere ([] :: [Filter PersistentThing]) + +selectOne :: SqlPersistT IO [Single Int] +selectOne = rawSql "SELECT 1" [] + +basicCrud :: SqlPersistT IO () +basicCrud = do + key <- insert (PersistentThing "created") + + created <- get key + liftIO $ created `shouldBe` Just (PersistentThing "created") + + update key [PersistentThingValue =. "updated"] + updated <- get key + liftIO $ updated `shouldBe` Just (PersistentThing "updated") + + delete key + deleted <- get key + liftIO $ deleted `shouldBe` Nothing From e8d114a32a13bdf09da58cb16c53e94d67299458 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 09:59:18 -0700 Subject: [PATCH 2/7] haskell is my friend --- Cargo.lock | 2 +- Cargo.toml | 2 +- integration/haskell/dev.sh | 8 +++ .../haskell/pgdog-haskell-integration.cabal | 3 +- integration/haskell/run.sh | 4 +- integration/haskell/test/Main.hs | 19 ++++- integration/pgdog.toml | 1 + pgdog/src/admin/mod.rs | 2 + pgdog/src/admin/parser.rs | 70 ++++++++++++------- pgdog/src/admin/set.rs | 11 +++ pgdog/src/admin/show_guc.rs | 47 +++++++++++++ 11 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 integration/haskell/dev.sh create mode 100644 pgdog/src/admin/show_guc.rs diff --git a/Cargo.lock b/Cargo.lock index 48f2cd894..e7b498ebb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3271,7 +3271,7 @@ dependencies = [ [[package]] name = "pg_raw_parse" version = "0.1.0" -source = "git+https://github.com/pgdogdev/pg_raw_parse.git?rev=8758803#8758803494e9f6eb4c4fbb47168aecc13614aa8f" +source = "git+https://github.com/pgdogdev/pg_raw_parse.git?rev=86e58cf#86e58cf493e239522f460cdfbdbdabbdeda2345d" dependencies = [ "bindgen 0.72.1", "cc", diff --git a/Cargo.toml b/Cargo.toml index 04c0abafc..22730a24c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ edition = "2024" pgdog-plugin = { path = "./pgdog-plugin", version = "0.4.0", default-features = false } pgdog-config = { path = "./pgdog-config", version = "0.1.0" } pgdog-postgres-types = { path = "./pgdog-postgres-types"} -pg_raw_parse = { git = "https://github.com/pgdogdev/pg_raw_parse.git", rev = "8758803" } +pg_raw_parse = { git = "https://github.com/pgdogdev/pg_raw_parse.git", rev = "86e58cf" } bon = "3.9" schemars = { version = "1.2.1", features = ["uuid1"] } serde_json = "1.0" diff --git a/integration/haskell/dev.sh b/integration/haskell/dev.sh new file mode 100644 index 000000000..8e8689f93 --- /dev/null +++ b/integration/haskell/dev.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +pushd "${SCRIPT_DIR}" +cabal test --test-show-details=direct +popd diff --git a/integration/haskell/pgdog-haskell-integration.cabal b/integration/haskell/pgdog-haskell-integration.cabal index 6a6a12201..8b2424cb6 100644 --- a/integration/haskell/pgdog-haskell-integration.cabal +++ b/integration/haskell/pgdog-haskell-integration.cabal @@ -15,4 +15,5 @@ test-suite persistent-integration monad-logger >=0.3 && <0.4, persistent >=2.18.1 && <2.19, persistent-postgresql >=2.14.3 && <2.15, - text >=1.2 && <3 + text >=1.2 && <3, + transformers >=0.5 && <0.7 diff --git a/integration/haskell/run.sh b/integration/haskell/run.sh index f2acea807..0a265e3b8 100755 --- a/integration/haskell/run.sh +++ b/integration/haskell/run.sh @@ -14,8 +14,6 @@ fi run_pgdog wait_for_pgdog -pushd "${SCRIPT_DIR}" -cabal test --test-show-details=direct -popd +bash ${SCRIPT_DIR}/dev.sh stop_pgdog diff --git a/integration/haskell/test/Main.hs b/integration/haskell/test/Main.hs index 3a46b6a0d..daa78e082 100644 --- a/integration/haskell/test/Main.hs +++ b/integration/haskell/test/Main.hs @@ -13,8 +13,10 @@ module Main (main) where +import Control.Exception (bracket_) import Control.Monad.IO.Class (liftIO) import Control.Monad.Logger (runNoLoggingT) +import Control.Monad.Trans.Reader (runReaderT) import Data.Text (Text) import Database.Persist import Database.Persist.Postgresql @@ -32,8 +34,23 @@ PersistentThing sql=haskell_persistent connectionString :: ConnectionString connectionString = "host=127.0.0.1 port=6432 user=pgdog password=pgdog dbname=pgdog sslmode=disable" +adminConnectionString :: ConnectionString +adminConnectionString = "host=127.0.0.1 port=6432 user=admin password=pgdog dbname=admin sslmode=disable" + main :: IO () -main = runNoLoggingT $ +main = + bracket_ + (runAdminCommand "SET read_write_strategy TO 'conservative'") + (runAdminCommand "RELOAD") + runTests + +runAdminCommand :: Text -> IO () +runAdminCommand command = runNoLoggingT $ + withPostgresqlConn adminConnectionString $ + runReaderT (rawExecute command []) + +runTests :: IO () +runTests = runNoLoggingT $ withPostgresqlPool connectionString 2 $ \pool -> liftIO $ do runSqlPool (runMigration migrateAll) pool diff --git a/integration/pgdog.toml b/integration/pgdog.toml index d5fd64d75..c69fed49a 100644 --- a/integration/pgdog.toml +++ b/integration/pgdog.toml @@ -27,6 +27,7 @@ reload_schema_on_ddl = false unique_id_function = "standard" auth_type = "scram" workers = 4 +query_log_stdout = true # log_dedup_window = 30_000 # log_dedup_threshold = diff --git a/pgdog/src/admin/mod.rs b/pgdog/src/admin/mod.rs index 3f36cada5..fbc3dcf40 100644 --- a/pgdog/src/admin/mod.rs +++ b/pgdog/src/admin/mod.rs @@ -29,6 +29,7 @@ pub mod show_bans; pub mod show_client_memory; pub mod show_clients; pub mod show_config; +pub mod show_guc; pub mod show_instance_id; pub mod show_listeners; pub mod show_lists; @@ -74,6 +75,7 @@ pub use show_bans::*; pub use show_client_memory::*; pub use show_clients::*; pub use show_config::*; +pub use show_guc::*; pub use show_instance_id::*; pub use show_listeners::*; pub use show_lists::*; diff --git a/pgdog/src/admin/parser.rs b/pgdog/src/admin/parser.rs index 15e004a97..52d7c7be7 100644 --- a/pgdog/src/admin/parser.rs +++ b/pgdog/src/admin/parser.rs @@ -1,5 +1,7 @@ //! Admin command parser. +use crate::admin::show_guc::get_show_variable; + use super::*; use tracing::debug; @@ -46,6 +48,7 @@ pub enum ParseResult { ShowTasks(ShowTasks), StopTask(StopTask), Cutover(Cutover), + Guc(ShowGuc), } impl ParseResult { @@ -94,6 +97,7 @@ impl ParseResult { ShowTasks(cmd) => cmd.execute().await, StopTask(cmd) => cmd.execute().await, Cutover(cmd) => cmd.execute().await, + Guc(cmd) => cmd.execute().await, } } @@ -142,6 +146,7 @@ impl ParseResult { ShowTasks(cmd) => cmd.name(), StopTask(cmd) => cmd.name(), Cutover(cmd) => cmd.name(), + Guc(cmd) => cmd.name(), } } } @@ -152,6 +157,42 @@ pub struct Parser; impl Parser { /// Parse the query and return a command we can execute. pub fn parse(sql: &str) -> Result { + // Handle SET separately because + // we're about to clobber valid SQL syntax below. + if is_set_statement(sql) { + return Ok(ParseResult::Set(Set::parse(sql)?)); + } + + if let Ok(show) = get_show_variable(sql) { + return Ok(match show.as_str() { + "clients" => ParseResult::ShowClients(ShowClients::parse(sql)?), + "pools" => ParseResult::ShowPools(ShowPools::parse(sql)?), + "bans" => ParseResult::ShowBans(ShowBans::parse(sql)?), + "config" => ParseResult::ShowConfig(ShowConfig::parse(sql)?), + "servers" => ParseResult::ShowServers(ShowServers::parse(sql)?), + "peers" => ParseResult::ShowPeers(ShowPeers::parse(sql)?), + "query_cache" => ParseResult::ShowQueryCache(ShowQueryCache::parse(sql)?), + "stats" => ParseResult::ShowStats(ShowStats::parse(sql)?), + "transactions" => ParseResult::ShowTransactions(ShowTransactions::parse(sql)?), + "mirrors" => ParseResult::ShowMirrors(ShowMirrors::parse(sql)?), + "version" => ParseResult::ShowVersion(ShowVersion::parse(sql)?), + "instance_id" => ParseResult::ShowInstanceId(ShowInstanceId::parse(sql)?), + "lists" => ParseResult::ShowLists(ShowLists::parse(sql)?), + "listeners" => ParseResult::ShowListeners(ShowListeners::parse(sql)?), + "prepared" => ParseResult::ShowPrepared(ShowPreparedStatements::parse(sql)?), + "replication" => ParseResult::ShowReplication(ShowReplication::parse(sql)?), + "replication_slots" => { + ParseResult::ShowReplicationSlots(ShowReplicationSlots::parse(sql)?) + } + "schema_sync" => ParseResult::ShowSchemaSync(ShowSchemaSync::parse(sql)?), + "table_copies" => ParseResult::ShowTableCopies(ShowTableCopies::parse(sql)?), + "tasks" => ParseResult::ShowTasks(ShowTasks::parse(sql)?), + variable => ParseResult::Guc(ShowGuc { + variable: variable.to_string(), + }), + }); + } + let sql = sql.trim().replace(";", "").to_lowercase(); let mut iter = sql.split(" "); @@ -162,12 +203,9 @@ impl Parser { "reload" => ParseResult::Reload(Reload::parse(&sql)?), "ban" | "unban" => ParseResult::Ban(Ban::parse(&sql)?), "healthcheck" => ParseResult::Healthcheck(Healthcheck::parse(&sql)?), + // These are not coevered by the show handler above + // because they are not valid SQL syntax. "show" => match iter.next().ok_or(Error::Syntax)?.trim() { - "clients" => ParseResult::ShowClients(ShowClients::parse(&sql)?), - "pools" => ParseResult::ShowPools(ShowPools::parse(&sql)?), - "bans" => ParseResult::ShowBans(ShowBans::parse(&sql)?), - "config" => ParseResult::ShowConfig(ShowConfig::parse(&sql)?), - "servers" => ParseResult::ShowServers(ShowServers::parse(&sql)?), "server" => match iter.next().ok_or(Error::Syntax)?.trim() { "memory" => ParseResult::ShowServerMemory(ShowServerMemory::parse(&sql)?), command => { @@ -182,23 +220,7 @@ impl Parser { return Err(Error::Syntax); } }, - "peers" => ParseResult::ShowPeers(ShowPeers::parse(&sql)?), - "query_cache" => ParseResult::ShowQueryCache(ShowQueryCache::parse(&sql)?), - "stats" => ParseResult::ShowStats(ShowStats::parse(&sql)?), - "transactions" => ParseResult::ShowTransactions(ShowTransactions::parse(&sql)?), - "mirrors" => ParseResult::ShowMirrors(ShowMirrors::parse(&sql)?), - "version" => ParseResult::ShowVersion(ShowVersion::parse(&sql)?), - "instance_id" => ParseResult::ShowInstanceId(ShowInstanceId::parse(&sql)?), - "lists" => ParseResult::ShowLists(ShowLists::parse(&sql)?), - "listeners" => ParseResult::ShowListeners(ShowListeners::parse(&sql)?), - "prepared" => ParseResult::ShowPrepared(ShowPreparedStatements::parse(&sql)?), - "replication" => ParseResult::ShowReplication(ShowReplication::parse(&sql)?), - "replication_slots" => { - ParseResult::ShowReplicationSlots(ShowReplicationSlots::parse(&sql)?) - } - "schema_sync" => ParseResult::ShowSchemaSync(ShowSchemaSync::parse(&sql)?), - "table_copies" => ParseResult::ShowTableCopies(ShowTableCopies::parse(&sql)?), - "tasks" => ParseResult::ShowTasks(ShowTasks::parse(&sql)?), + command => { debug!("unknown admin show command: '{}'", command); return Err(Error::Syntax); @@ -227,10 +249,6 @@ impl Parser { "cutover" => ParseResult::Cutover(Cutover::parse(&sql)?), "probe" => ParseResult::Probe(Probe::parse(&sql)?), "maintenance" => ParseResult::MaintenanceMode(MaintenanceMode::parse(&sql)?), - // TODO: This is not ready yet. We have a race and - // also the changed settings need to be propagated - // into the pools. - "set" => ParseResult::Set(Set::parse(&sql)?), command => { debug!("unknown admin command: {}", command); return Err(Error::Syntax); diff --git a/pgdog/src/admin/set.rs b/pgdog/src/admin/set.rs index a06c1514b..5f53f0e48 100644 --- a/pgdog/src/admin/set.rs +++ b/pgdog/src/admin/set.rs @@ -13,6 +13,17 @@ pub struct Set { value: String, } +pub(super) fn is_set_statement(sql: &str) -> bool { + is_set_or_error(sql).unwrap_or_default() +} + +fn is_set_or_error(sql: &str) -> Result { + let stmt = pg_raw_parse::parse(sql).map_err(|_| Error::Syntax)?; + let root = stmt.stmts().next().ok_or(Error::Syntax)?; + + Ok(matches!(root, Node::VariableSetStmt(_))) +} + #[async_trait] impl Command for Set { fn name(&self) -> String { diff --git a/pgdog/src/admin/show_guc.rs b/pgdog/src/admin/show_guc.rs new file mode 100644 index 000000000..fb4d23512 --- /dev/null +++ b/pgdog/src/admin/show_guc.rs @@ -0,0 +1,47 @@ +use crate::{ + net::{DataRow, Field, Protocol, RowDescription}, + util::pgdog_version, +}; + +use super::*; +use pg_raw_parse::Node; + +pub(super) fn get_show_variable(sql: &str) -> Result { + let stmt = pg_raw_parse::parse(sql).map_err(|_| Error::Syntax)?; + let root = stmt.stmts().next().ok_or(Error::Syntax)?; + + if let Node::VariableShowStmt(stmt) = root { + Ok(stmt.name().unwrap_or_default().to_owned()) + } else { + Err(Error::Syntax) + } +} + +pub struct ShowGuc { + pub(super) variable: String, +} + +#[async_trait] +impl Command for ShowGuc { + fn name(&self) -> String { + "SHOW".into() + } + + fn parse(_sql: &str) -> Result { + unreachable!("ShowGuc is initialized manually") + } + + async fn execute(&self) -> Result, Error> { + let value = match self.variable.as_str() { + "version" => pgdog_version(), + "server_version" => pg_raw_parse::postgres_version().to_string(), + _ => String::new(), + }; + + let rd = RowDescription::new(&[Field::text(&self.variable)]); + let mut dr = DataRow::new(); + dr.add(&value); + + Ok(vec![rd.message()?, dr.message()?]) + } +} From ef8678f9fcc3d215dcc67b4087029bd8d491c925 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 12:15:39 -0700 Subject: [PATCH 3/7] pg_parse_raw --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e7b498ebb..81413f808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3271,7 +3271,7 @@ dependencies = [ [[package]] name = "pg_raw_parse" version = "0.1.0" -source = "git+https://github.com/pgdogdev/pg_raw_parse.git?rev=86e58cf#86e58cf493e239522f460cdfbdbdabbdeda2345d" +source = "git+https://github.com/pgdogdev/pg_raw_parse.git?rev=6870860#68708609b9bf6b0569257026b2e6d4c1b1748fef" dependencies = [ "bindgen 0.72.1", "cc", diff --git a/Cargo.toml b/Cargo.toml index 22730a24c..de730e54d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ edition = "2024" pgdog-plugin = { path = "./pgdog-plugin", version = "0.4.0", default-features = false } pgdog-config = { path = "./pgdog-config", version = "0.1.0" } pgdog-postgres-types = { path = "./pgdog-postgres-types"} -pg_raw_parse = { git = "https://github.com/pgdogdev/pg_raw_parse.git", rev = "86e58cf" } +pg_raw_parse = { git = "https://github.com/pgdogdev/pg_raw_parse.git", rev = "6870860" } bon = "3.9" schemars = { version = "1.2.1", features = ["uuid1"] } serde_json = "1.0" From 9d417d8078097e68f4b3784f1dd5d60de8529cbb Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 12:16:45 -0700 Subject: [PATCH 4/7] remove diff --- integration/pgdog.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/integration/pgdog.toml b/integration/pgdog.toml index c69fed49a..d5fd64d75 100644 --- a/integration/pgdog.toml +++ b/integration/pgdog.toml @@ -27,7 +27,6 @@ reload_schema_on_ddl = false unique_id_function = "standard" auth_type = "scram" workers = 4 -query_log_stdout = true # log_dedup_window = 30_000 # log_dedup_threshold = From d4d2e4fe779ec7122378f993e670934e1a1d6798 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 12:31:18 -0700 Subject: [PATCH 5/7] fix reset handling and add more coverage --- pgdog/src/admin/parser.rs | 78 +++++++++++++++++++++++++++++++++++++++ pgdog/src/admin/set.rs | 11 +++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/pgdog/src/admin/parser.rs b/pgdog/src/admin/parser.rs index 52d7c7be7..c6ab91fa5 100644 --- a/pgdog/src/admin/parser.rs +++ b/pgdog/src/admin/parser.rs @@ -261,6 +261,84 @@ impl Parser { mod tests { use super::{Error, ParseResult, Parser}; + macro_rules! assert_parses { + ($sql:literal, $variant:pat) => { + assert!( + matches!(Parser::parse($sql), Ok($variant)), + "failed to parse `{}`", + $sql + ); + }; + } + + #[test] + fn parses_pool_control_commands() { + assert_parses!("PAUSE", ParseResult::Pause(_)); + assert_parses!("RESUME", ParseResult::Pause(_)); + assert_parses!("RECONNECT", ParseResult::Reconnect(_)); + assert_parses!("RELOAD", ParseResult::Reload(_)); + assert_parses!("SHUTDOWN", ParseResult::Shutdown(_)); + assert_parses!("BAN", ParseResult::Ban(_)); + assert_parses!("UNBAN", ParseResult::Ban(_)); + assert_parses!("HEALTHCHECK", ParseResult::Healthcheck(_)); + assert_parses!( + "PROBE postgres://postgres@localhost/postgres", + ParseResult::Probe(_) + ); + assert_parses!("MAINTENANCE ON", ParseResult::MaintenanceMode(_)); + assert_parses!("SET query_timeout TO '1000'", ParseResult::Set(_)); + } + + #[test] + fn parses_show_commands() { + assert_parses!("SHOW CLIENTS", ParseResult::ShowClients(_)); + assert_parses!("SHOW POOLS", ParseResult::ShowPools(_)); + assert_parses!("SHOW BANS", ParseResult::ShowBans(_)); + assert_parses!("SHOW CONFIG", ParseResult::ShowConfig(_)); + assert_parses!("SHOW SERVERS", ParseResult::ShowServers(_)); + assert_parses!("SHOW PEERS", ParseResult::ShowPeers(_)); + assert_parses!("SHOW QUERY_CACHE", ParseResult::ShowQueryCache(_)); + assert_parses!("SHOW STATS", ParseResult::ShowStats(_)); + assert_parses!("SHOW TRANSACTIONS", ParseResult::ShowTransactions(_)); + assert_parses!("SHOW MIRRORS", ParseResult::ShowMirrors(_)); + assert_parses!("SHOW VERSION", ParseResult::ShowVersion(_)); + assert_parses!("SHOW INSTANCE_ID", ParseResult::ShowInstanceId(_)); + assert_parses!("SHOW LISTS", ParseResult::ShowLists(_)); + assert_parses!("SHOW LISTENERS", ParseResult::ShowListeners(_)); + assert_parses!("SHOW PREPARED", ParseResult::ShowPrepared(_)); + assert_parses!("SHOW REPLICATION", ParseResult::ShowReplication(_)); + assert_parses!( + "SHOW REPLICATION_SLOTS", + ParseResult::ShowReplicationSlots(_) + ); + assert_parses!("SHOW SCHEMA_SYNC", ParseResult::ShowSchemaSync(_)); + assert_parses!("SHOW TABLE_COPIES", ParseResult::ShowTableCopies(_)); + assert_parses!("SHOW TASKS", ParseResult::ShowTasks(_)); + assert_parses!("SHOW SERVER MEMORY", ParseResult::ShowServerMemory(_)); + assert_parses!("SHOW CLIENT MEMORY", ParseResult::ShowClientMemory(_)); + assert_parses!("SHOW server_version", ParseResult::Guc(_)); + } + + #[test] + fn parses_schema_and_replication_commands() { + assert_parses!("SETUP SCHEMA", ParseResult::SetupSchema(_)); + assert_parses!("RESHARD source target publication", ParseResult::Reshard(_)); + assert_parses!( + "SCHEMA_SYNC pre source target publication", + ParseResult::SchemaSync(_) + ); + assert_parses!( + "COPY_DATA source target publication", + ParseResult::CopyData(_) + ); + assert_parses!( + "REPLICATE source target publication", + ParseResult::Replicate(_) + ); + assert_parses!("STOP_TASK 1", ParseResult::StopTask(_)); + assert_parses!("CUTOVER", ParseResult::Cutover(_)); + } + #[test] fn parses_show_clients_command() { let result = Parser::parse("SHOW CLIENTS;"); diff --git a/pgdog/src/admin/set.rs b/pgdog/src/admin/set.rs index 5f53f0e48..66695dd58 100644 --- a/pgdog/src/admin/set.rs +++ b/pgdog/src/admin/set.rs @@ -5,7 +5,10 @@ use crate::{ }; use super::prelude::*; -use pg_raw_parse::Node; +use pg_raw_parse::{ + Node, + raw::VariableSetKind::{VAR_RESET, VAR_RESET_ALL}, +}; use serde::de::DeserializeOwned; pub struct Set { @@ -21,7 +24,11 @@ fn is_set_or_error(sql: &str) -> Result { let stmt = pg_raw_parse::parse(sql).map_err(|_| Error::Syntax)?; let root = stmt.stmts().next().ok_or(Error::Syntax)?; - Ok(matches!(root, Node::VariableSetStmt(_))) + Ok(if let Node::VariableSetStmt(stmt) = root { + !matches!(stmt.kind, VAR_RESET_ALL | VAR_RESET) + } else { + false + }) } #[async_trait] From 93cb399af423ca1274bd7896bc546e0d87e131bd Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 15:27:13 -0700 Subject: [PATCH 6/7] fmt --- pgdog/src/admin/parser.rs | 18 +++++++++++++++++- pgdog/src/admin/show_guc.rs | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pgdog/src/admin/parser.rs b/pgdog/src/admin/parser.rs index c6ab91fa5..88a3392db 100644 --- a/pgdog/src/admin/parser.rs +++ b/pgdog/src/admin/parser.rs @@ -203,9 +203,13 @@ impl Parser { "reload" => ParseResult::Reload(Reload::parse(&sql)?), "ban" | "unban" => ParseResult::Ban(Ban::parse(&sql)?), "healthcheck" => ParseResult::Healthcheck(Healthcheck::parse(&sql)?), - // These are not coevered by the show handler above + // These are not covered by the show handler above // because they are not valid SQL syntax. "show" => match iter.next().ok_or(Error::Syntax)?.trim() { + // These two are duplicated because they support selecting columns from their output. + "clients" => ParseResult::ShowClients(ShowClients::parse(&sql)?), + "servers" => ParseResult::ShowServers(ShowServers::parse(&sql)?), + "server" => match iter.next().ok_or(Error::Syntax)?.trim() { "memory" => ParseResult::ShowServerMemory(ShowServerMemory::parse(&sql)?), command => { @@ -319,6 +323,18 @@ mod tests { assert_parses!("SHOW server_version", ParseResult::Guc(_)); } + #[test] + fn parses_show_commands_with_selected_columns() { + assert_parses!( + "SHOW CLIENTS prepared_statements, application_name", + ParseResult::ShowClients(_) + ); + assert_parses!( + "SHOW SERVERS remote_pid, application_name", + ParseResult::ShowServers(_) + ); + } + #[test] fn parses_schema_and_replication_commands() { assert_parses!("SETUP SCHEMA", ParseResult::SetupSchema(_)); diff --git a/pgdog/src/admin/show_guc.rs b/pgdog/src/admin/show_guc.rs index fb4d23512..353776bae 100644 --- a/pgdog/src/admin/show_guc.rs +++ b/pgdog/src/admin/show_guc.rs @@ -28,7 +28,7 @@ impl Command for ShowGuc { } fn parse(_sql: &str) -> Result { - unreachable!("ShowGuc is initialized manually") + unreachable!("ShowGuc must be initialized manually") } async fn execute(&self) -> Result, Error> { From 4f64f2d6082bfc7c7eec2904d0779d78e3533677 Mon Sep 17 00:00:00 2001 From: Lev Kokotov Date: Thu, 13 Aug 2026 15:53:17 -0700 Subject: [PATCH 7/7] you better be worth it --- integration/haskell/dev.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/integration/haskell/dev.sh b/integration/haskell/dev.sh index 8e8689f93..665416550 100644 --- a/integration/haskell/dev.sh +++ b/integration/haskell/dev.sh @@ -4,5 +4,26 @@ set -euo pipefail SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) pushd "${SCRIPT_DIR}" + +has_required_version() { + cabal list persistent-postgresql --simple-output | + grep -Fqx 'persistent-postgresql 2.14.3.0' +} + +for attempt in 1 2 3; do + if has_required_version; then + break + fi + + echo "Hackage index is stale; refreshing (attempt ${attempt}/3)" >&2 + cabal update || true + sleep $((attempt * 5)) +done + +if ! has_required_version; then + echo "Hackage index does not contain persistent-postgresql 2.14.3.0 after 3 refresh attempts" >&2 + exit 1 +fi + cabal test --test-show-details=direct popd