packages feed

hasql-1.10.3.4: src/library-tests/Sharing/ByUnit/Encoders/CustomSpec.hs

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

import Data.HashSet qualified as HashSet
import Data.Text.Encoding (encodeUtf8)
import Hasql.Connection qualified as Connection
import Hasql.Decoders qualified as Decoders
import Hasql.Encoders (TypeInfo (..))
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 TextBuilder qualified
import Prelude

spec :: SpecWith (Text, Word16)
spec = do
  describe "Basic custom encoders" do
    it "encodes a custom type with runtime OID lookup" \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 custom encoder with runtime OID lookup
          Session.statement "beta"
            $ Statement.preparable
              (mconcat ["select ($1 :: ", enumName, ") = 'beta' :: ", enumName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          Nothing
                          enumName
                          Nothing
                          []
                          (\_ val -> encodeUtf8 val)
                          id
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "encodes a custom type with static OIDs" \config -> do
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Test custom encoder with static OIDs for text (type OID 25, array OID 1009)
          Session.statement "hello"
            $ Statement.preparable
              "select $1::text = 'hello'::text"
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          Nothing
                          "text"
                          (Just (TypeInfo 25 1009))
                          []
                          (\_ val -> encodeUtf8 val)
                          id
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "encodes with dependent type OID requests" \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', 'large')"])
              mempty
              Decoders.noResult
          -- Test custom encoder that requests OID of the enum type itself
          Session.statement "large"
            $ Statement.preparable
              (mconcat ["select ($1 :: ", enumName, ") = 'large' :: ", enumName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          Nothing
                          enumName
                          Nothing
                          [(Nothing, enumName)]
                          ( \lookupOid val -> do
                              let enumTypeInfo = lookupOid (Nothing, enumName)
                                  enumOidScalar = toBaseOid enumTypeInfo
                              -- Verify we got a valid OID (non-zero)
                              if enumOidScalar > 0
                                then encodeUtf8 val
                                else error "Failed to resolve enum OID"
                          )
                          id
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

  describe "Error handling" do
    it "detects missing types in custom encoders" \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.custom
                          Nothing
                          "nonexistent_custom_type"
                          Nothing
                          []
                          (\_ val -> encodeUtf8 val)
                          id
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

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

    it "detects missing dependent types in custom encoders" \config -> do
      customTypeName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create a custom type
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", customTypeName, " as (id int4)"])
              mempty
              Decoders.noResult
          -- Try to encode it but request a non-existent dependent type
          Session.statement (42 :: Int32)
            $ Statement.preparable
              (mconcat ["select $1 :: ", customTypeName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          Nothing
                          customTypeName
                          Nothing
                          [(Nothing, "nonexistent_dependency")]
                          (\_ val -> encodeUtf8 (TextBuilder.toText (TextBuilder.decimal val)))
                          (TextBuilder.toText . TextBuilder.decimal)
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.text)))

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

  describe "Roundtrip tests" do
    it "roundtrips custom encoded and decoded values" \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 ('one', 'two', 'three')"])
              mempty
              Decoders.noResult
          -- Test roundtrip using custom encoder and decoder
          Session.statement "three"
            $ Statement.preparable
              (mconcat ["select $1 :: ", enumName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          Nothing
                          enumName
                          Nothing
                          []
                          (\_ val -> encodeUtf8 val)
                          id
                      )
                  )
              )
              ( Decoders.singleRow
                  ( Decoders.column
                      ( Decoders.nonNullable
                          ( Decoders.custom
                              Nothing
                              enumName
                              Nothing
                              []
                              (\_ bytes -> Right bytes)
                          )
                      )
                  )
              )
        result `shouldBe` Right "three"

    it "roundtrips multiple values" \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 ('first', 'second', 'third')"])
              mempty
              Decoders.noResult
          -- Test roundtrip for multiple values
          r1 <-
            Session.statement "first"
              $ Statement.preparable
                (mconcat ["select $1 :: ", enumName])
                ( Encoders.param
                    ( Encoders.nonNullable
                        ( Encoders.custom
                            Nothing
                            enumName
                            Nothing
                            []
                            (\_ val -> encodeUtf8 val)
                            id
                        )
                    )
                )
                ( Decoders.singleRow
                    ( Decoders.column
                        ( Decoders.nonNullable
                            ( Decoders.custom
                                Nothing
                                enumName
                                Nothing
                                []
                                (\_ bytes -> Right bytes)
                            )
                        )
                    )
                )
          r2 <-
            Session.statement "third"
              $ Statement.preparable
                (mconcat ["select $1 :: ", enumName])
                ( Encoders.param
                    ( Encoders.nonNullable
                        ( Encoders.custom
                            Nothing
                            enumName
                            Nothing
                            []
                            (\_ val -> encodeUtf8 val)
                            id
                        )
                    )
                )
                ( Decoders.singleRow
                    ( Decoders.column
                        ( Decoders.nonNullable
                            ( Decoders.custom
                                Nothing
                                enumName
                                Nothing
                                []
                                (\_ bytes -> Right bytes)
                            )
                        )
                    )
                )
          return (r1, r2)
        result `shouldBe` Right ("first", "third")

  describe "Schema-qualified types" do
    it "encodes custom types from specific schemas" \config -> do
      schemaName <- Scripts.generateSymname
      typeName <- Scripts.generateSymname
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          -- Create schema
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create schema ", schemaName])
              mempty
              Decoders.noResult
          -- Create enum type in that schema
          Session.statement ()
            $ Statement.preparable
              (mconcat ["create type ", schemaName, ".", typeName, " as enum ('x', 'y', 'z')"])
              mempty
              Decoders.noResult
          -- Test custom encoder with schema qualification
          Session.statement "z"
            $ Statement.preparable
              (mconcat ["select ($1 :: ", schemaName, ".", typeName, ") = 'z' :: ", schemaName, ".", typeName])
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          (Just schemaName)
                          typeName
                          Nothing
                          []
                          (\_ val -> encodeUtf8 val)
                          id
                      )
                  )
              )
              (Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool)))
        result `shouldBe` Right True

    it "detects missing types in non-existent schemas" \config -> do
      Scripts.onPreparableConnection config \connection -> do
        result <- Connection.use connection do
          Session.statement "test"
            $ Statement.preparable
              "select $1"
              ( Encoders.param
                  ( Encoders.nonNullable
                      ( Encoders.custom
                          (Just "nonexistent_schema")
                          "nonexistent_type"
                          Nothing
                          []
                          (\_ val -> encodeUtf8 val)
                          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)