packages feed

hasql-1.10: src/library-tests/Sharing/ByUnit/Encoders/EnumSpec.hs

module Sharing.ByUnit.Encoders.EnumSpec (spec) where

import Data.HashSet qualified as HashSet
import Hasql.Connection qualified as Connection
import Hasql.Decoders qualified as Decoders
import Hasql.Encoders qualified as Encoders
import Hasql.Errors qualified as Errors
import Hasql.Session qualified as Session
import Hasql.Statement qualified as Statement
import Helpers.Scripts qualified as Scripts
import Test.Hspec
import Prelude

spec :: SpecWith (Text, Word16)
spec = do
  describe "Simple enums" do
    it "encodes a simple named enum and compares with static value" \config -> do
      enumName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('sad', 'ok', 'happy')"])
              mempty
              Decoders.noResult
          -- Test encoding by comparing with static value
          Session.statement "ok"
            $ Statement.preparable
              (mconcat ["select ($1 :: ", enumName, ") = 'ok' :: ", enumName])
              (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "encodes and roundtrips a simple named enum" \config -> do
      enumName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('sad', 'ok', 'happy')"])
              mempty
              Decoders.noResult
          -- Test roundtrip
          Session.statement "happy"
            $ Statement.preparable
              (mconcat ["select $1 :: ", enumName])
              (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable (Decoders.enum Nothing enumName (Just . id)))))
        result `shouldBe` Right "happy"

  describe "Enums in composites" do
    it "encodes enums nested in named composites" \config -> do
      enumName <- Scripts.generateSymname
      compositeName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('red', 'green', 'blue')"])
              mempty
              Decoders.noResult
          -- Create composite type with enum
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", compositeName, " as (id int8, color ", enumName, ")"])
              mempty
              Decoders.noResult
          -- Test encoding
          Session.statement (42 :: Int64, "green")
            $ Statement.preparable
              (mconcat ["select ($1 :: ", compositeName, ") = (42, 'green') :: ", compositeName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.composite
                          Nothing
                          compositeName
                          ( divide
                              (\(a, b) -> (a, b))
                              (Encoders.field (Encoders.nonNullable Encoders.int8))
                              (Encoders.field (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
                          )
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "roundtrips enums nested in named composites" \config -> do
      enumName <- Scripts.generateSymname
      compositeName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('red', 'green', 'blue')"])
              mempty
              Decoders.noResult
          -- Create composite type with enum
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", compositeName, " as (id int8, color ", enumName, ")"])
              mempty
              Decoders.noResult
          -- Test roundtrip
          Session.statement (42 :: Int64, "blue")
            $ Statement.preparable
              (mconcat ["select $1 :: ", compositeName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.composite
                          Nothing
                          compositeName
                          ( divide
                              (\(a, b) -> (a, b))
                              (Encoders.field (Encoders.nonNullable Encoders.int8))
                              (Encoders.field (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
                          )
                      )
                  )
              )
              ( Decoders.singleRow
                  ( Decoders.column
                      ( Decoders.nonNullable
                          ( Decoders.composite
                              Nothing
                              compositeName
                              ( (,)
                                  <$> Decoders.field (Decoders.nonNullable Decoders.int8)
                                  <*> Decoders.field (Decoders.nonNullable (Decoders.enum Nothing enumName (Just . id)))
                              )
                          )
                      )
                  )
              )
        result `shouldBe` Right (42 :: Int64, "blue")

  describe "Arrays of enums" do
    it "encodes arrays of named enums" \config -> do
      enumName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('small', 'medium', 'large')"])
              mempty
              Decoders.noResult
          -- Test array encoding
          Session.statement ["small", "large", "medium"]
            $ Statement.preparable
              (mconcat ["select ($1 :: ", enumName, "[]) = array['small', 'large', 'medium'] :: ", enumName, "[]"])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.array
                          ( Encoders.dimension
                              foldl'
                              (Encoders.element (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
                          )
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "roundtrips arrays of named enums" \config -> do
      enumName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('alpha', 'beta', 'gamma')"])
              mempty
              Decoders.noResult
          -- Test roundtrip
          Session.statement ["beta", "alpha", "gamma"]
            $ Statement.preparable
              (mconcat ["select $1 :: ", enumName, "[]"])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.array
                          ( Encoders.dimension
                              foldl'
                              (Encoders.element (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
                          )
                      )
                  )
              )
              ( Decoders.singleRow
                  ( Decoders.column
                      ( Decoders.nonNullable
                          ( Decoders.array
                              ( Decoders.dimension
                                  replicateM
                                  (Decoders.element (Decoders.nonNullable (Decoders.enum Nothing enumName (Just . id))))
                              )
                          )
                      )
                  )
              )
        result `shouldBe` Right ["beta", "alpha", "gamma"]

  describe "OID lookup verification" do
    it "requests OID for named enums (verified by successful execution)" \config -> do
      -- This test verifies that OID lookup happens by ensuring a named enum
      -- type works correctly - if OID lookup didn't happen, the statement would fail
      enumName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create enum type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", enumName, " as enum ('first', 'second')"])
              mempty
              Decoders.noResult
          -- Use named enum - this requires OID lookup to succeed
          Session.statement "second"
            $ Statement.preparable
              (mconcat ["select $1 :: ", enumName])
              (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing enumName id)))
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable (Decoders.enum Nothing enumName (Just . id)))))
        result `shouldBe` Right "second"

  it "handles enum encoding and decoding" \config -> do
    name <- Scripts.generateSymname
    Scripts.onPreparableConnection config \connection -> do
      result <- Connection.use connection do
        -- First create the enum type
        Session.statement ()
          $ Statement.preparable
            (mconcat ["create type ", name, " as enum ('sad', 'ok', 'happy')"])
            mempty
            Decoders.noResult
        -- Then test encoding and decoding
        Session.statement "ok"
          $ Statement.preparable
            (mconcat ["select ($1 :: ", name, ")"])
            (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing name id)))
            (Decoders.singleRow (Decoders.column (Decoders.nonNullable (Decoders.enum Nothing name (Just . id)))))
      result `shouldBe` Right "ok"

  it "detects attempts to encode non-existent enum types" \config -> do
    Scripts.onPreparableConnection config \connection -> do
      result <- Connection.use connection do
        Session.statement "test_value"
          $ Statement.preparable
            "select $1"
            (Encoders.param (Encoders.nonNullable (Encoders.enum Nothing "this_enum_does_not_exist_in_db" id)))
            (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

      case result of
        Left (Errors.MissingTypesSessionError missingTypes) ->
          missingTypes `shouldBe` HashSet.fromList [(Nothing, "this_enum_does_not_exist_in_db")]
        _ ->
          expectationFailure ("Unexpected result: " <> show result)

  describe "Namespaced" do
    it "detects attempts to use non-existent type in non-existent schema" \config -> do
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          Session.statement "test"
            $ Statement.preparable
              "select $1::nonexistent_schema.nonexistent_type"
              (Encoders.param (Encoders.nonNullable (Encoders.enum (Just "nonexistent_schema") "nonexistent_type" id)))
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

        case result of
          Left (Errors.MissingTypesSessionError missingTypes) ->
            missingTypes `shouldBe` HashSet.fromList [(Just "nonexistent_schema", "nonexistent_type")]
          _ ->
            expectationFailure ("Unexpected result: " <> show result)

    it "detects attempts to use non-existent type in existing schema" \config -> do
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          Session.statement "test"
            $ Statement.preparable
              "select $1::public.this_type_does_not_exist"
              (Encoders.param (Encoders.nonNullable (Encoders.enum (Just "public") "this_type_does_not_exist" id)))
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

        -- The statement should fail when trying to use a non-existent type in existing schema
        case result of
          Left (Errors.MissingTypesSessionError missingTypes) -> do
            missingTypes `shouldBe` HashSet.fromList [(Just "public", "this_type_does_not_exist")]
          _ ->
            expectationFailure ("Unexpected result: " <> show result)

  it "detects attempts to encode arrays of non-existent enum types" \config -> do
    Scripts.onPreparableConnection config \connection -> do
      result <- Connection.use connection do
        Session.statement ["val1", "val2"]
          $ Statement.preparable
            "select $1::nonexistent_array_enum[]"
            ( Encoders.param
                ( Encoders.nonNullable
                    ( Encoders.array
                        ( Encoders.dimension
                            foldl'
                            (Encoders.element (Encoders.nonNullable (Encoders.enum Nothing "nonexistent_array_enum" id)))
                        )
                    )
                )
            )
            (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

      case result of
        Left (Errors.MissingTypesSessionError missingTypes) ->
          missingTypes `shouldBe` HashSet.fromList [(Nothing, "nonexistent_array_enum")]
        _ ->
          expectationFailure ("Unexpected result: " <> show result)