valiant-cli-0.1.0.0: test/Valiant/CLI/NamedParamsSpec.hs
module Valiant.CLI.NamedParamsSpec (spec) where
import Data.ByteString.Char8 qualified as BS8
import Valiant.CLI.NamedParams
import Test.Hspec
spec :: Spec
spec = do
describe "preprocessNamedParams" $ do
-- Basic functionality
it "replaces a single :name with $1" $ do
let (sql, mapping) = preprocessNamedParams "SELECT * FROM users WHERE id = :userId"
sql `shouldBe` "SELECT * FROM users WHERE id = $1"
mapping `shouldBe` [("userId", 1)]
it "replaces multiple distinct :names" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM users WHERE org_id = :orgId AND role = :role AND active = :active"
sql `shouldBe` "SELECT * FROM users WHERE org_id = $1 AND role = $2 AND active = $3"
mapping `shouldBe` [("active", 3), ("orgId", 1), ("role", 2)]
it "reuses the same $N for repeated :name" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE a = :x OR b = :x"
sql `shouldBe` "SELECT * FROM t WHERE a = $1 OR b = $1"
mapping `shouldBe` [("x", 1)]
it "handles mixed repeated and distinct names" $ do
let (sql, _mapping) = preprocessNamedParams
"SELECT * FROM t WHERE a = :x AND b = :y AND c = :x"
sql `shouldBe` "SELECT * FROM t WHERE a = $1 AND b = $2 AND c = $1"
-- No-op cases
it "returns original SQL unchanged when no named params" $ do
let input = "SELECT * FROM users WHERE id = $1"
let (sql, mapping) = preprocessNamedParams input
sql `shouldBe` input
mapping `shouldBe` []
it "returns empty mapping for parameterless queries" $ do
let (sql, mapping) = preprocessNamedParams "SELECT 1"
sql `shouldBe` "SELECT 1"
mapping `shouldBe` []
-- Postgres cast (::) handling
it "does not treat :: as a named parameter" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT :val::text FROM t WHERE id = :id"
sql `shouldBe` "SELECT $1::text FROM t WHERE id = $2"
mapping `shouldBe` [("id", 2), ("val", 1)]
it "handles multiple casts" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT :a::int4, :b::text"
sql `shouldBe` "SELECT $1::int4, $2::text"
mapping `shouldBe` [("a", 1), ("b", 2)]
it "handles cast without named param" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT $1::text"
sql `shouldBe` "SELECT $1::text"
mapping `shouldBe` []
-- String literal handling
it "does not substitute inside string literals" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE name = :name AND note = ':not_a_param'"
sql `shouldBe` "SELECT * FROM t WHERE name = $1 AND note = ':not_a_param'"
mapping `shouldBe` [("name", 1)]
it "handles escaped single quotes in strings" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE name = 'it''s :not_a_param' AND id = :id"
sql `shouldBe` "SELECT * FROM t WHERE name = 'it''s :not_a_param' AND id = $1"
mapping `shouldBe` [("id", 1)]
-- Quoted identifier handling
it "does not substitute inside quoted identifiers" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT \":notParam\" FROM t WHERE id = :id"
sql `shouldBe` "SELECT \":notParam\" FROM t WHERE id = $1"
mapping `shouldBe` [("id", 1)]
-- Comment handling
it "does not substitute inside line comments" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t -- WHERE :name = 'foo'\nWHERE id = :id"
sql `shouldBe` "SELECT * FROM t -- WHERE :name = 'foo'\nWHERE id = $1"
mapping `shouldBe` [("id", 1)]
it "does not substitute inside block comments" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * /* :notParam */ FROM t WHERE id = :id"
sql `shouldBe` "SELECT * /* :notParam */ FROM t WHERE id = $1"
mapping `shouldBe` [("id", 1)]
-- Naming conventions
it "supports underscored parameter names" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE org_id = :org_id"
sql `shouldBe` "SELECT * FROM t WHERE org_id = $1"
mapping `shouldBe` [("org_id", 1)]
it "supports names starting with underscore" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE id = :_internal"
sql `shouldBe` "SELECT * FROM t WHERE id = $1"
mapping `shouldBe` [("_internal", 1)]
it "supports names with digits" $ do
let (sql, mapping) = preprocessNamedParams
"SELECT * FROM t WHERE id = :param1 AND val = :param2"
sql `shouldBe` "SELECT * FROM t WHERE id = $1 AND val = $2"
mapping `shouldBe` [("param1", 1), ("param2", 2)]
-- Edge cases
it "handles :name at end of input" $ do
let (sql, mapping) = preprocessNamedParams "SELECT * FROM t WHERE id = :id"
sql `shouldBe` "SELECT * FROM t WHERE id = $1"
mapping `shouldBe` [("id", 1)]
it "handles :name at start of input" $ do
let (sql, mapping) = preprocessNamedParams ":val"
sql `shouldBe` "$1"
mapping `shouldBe` [("val", 1)]
it "handles adjacent :names" $ do
let (sql, _) = preprocessNamedParams "VALUES (:a,:b,:c)"
sql `shouldBe` "VALUES ($1,$2,$3)"
it "does not treat bare colon as param" $ do
let (sql, mapping) = preprocessNamedParams "SELECT * FROM t WHERE time > '12:30:00'"
sql `shouldBe` "SELECT * FROM t WHERE time > '12:30:00'"
mapping `shouldBe` []
it "does not treat colon followed by digit as param" $ do
let (sql, mapping) = preprocessNamedParams "SELECT * FROM t WHERE x = :1bad"
-- :1 is not a valid param name (starts with digit)
sql `shouldBe` "SELECT * FROM t WHERE x = :1bad"
mapping `shouldBe` []
-- Realistic queries
it "handles a realistic INSERT" $ do
let (sql, mapping) = preprocessNamedParams
"INSERT INTO users (name, email, org_id) VALUES (:name, :email, :orgId)"
sql `shouldBe` "INSERT INTO users (name, email, org_id) VALUES ($1, $2, $3)"
length mapping `shouldBe` 3
it "handles a realistic UPDATE" $ do
let (sql, mapping) = preprocessNamedParams
"UPDATE users SET name = :name, email = :email WHERE id = :userId"
sql `shouldBe` "UPDATE users SET name = $1, email = $2 WHERE id = $3"
length mapping `shouldBe` 3
it "handles a CTE with named params" $ do
let (sql, _) = preprocessNamedParams
"WITH active AS (SELECT * FROM users WHERE active = :active)\n\
\SELECT * FROM active WHERE org_id = :orgId"
BS8.isInfixOf "$1" sql `shouldBe` True
BS8.isInfixOf "$2" sql `shouldBe` True
BS8.isInfixOf ":active" sql `shouldBe` False
BS8.isInfixOf ":orgId" sql `shouldBe` False