packages feed

postgresql-types-0.1.3: src/library/PostgresqlTypes/Citext.hs

module PostgresqlTypes.Citext
  ( Citext,

    -- * Accessors
    toText,

    -- * Constructors
    normalizeFromText,
    refineFromText,
  )
where

import qualified Data.Attoparsec.Text as Attoparsec
import Data.String
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text.Encoding
import PostgresqlTypes.Algebra
import PostgresqlTypes.Prelude hiding (Text)
import PostgresqlTypes.Via
import qualified PtrPeeker
import qualified PtrPoker.Write as Write
import qualified Test.QuickCheck as QuickCheck
import qualified TextBuilder

-- | PostgreSQL @citext@ type. Case-insensitive variable-length character string.
-- Requires the @citext@ extension to be installed in PostgreSQL.
--
-- [PostgreSQL docs](https://www.postgresql.org/docs/18/citext.html).
--
-- The value is stored as UTF-8 text, identical to @text@. Case-insensitivity
-- is a property of comparisons, not of storage. NUL characters are not allowed.
newtype Citext = Citext Text.Text
  deriving newtype (Eq, Ord, Hashable)
  deriving (Show, Read, IsString) via (ViaIsScalar Citext)

instance Arbitrary Citext where
  arbitrary =
    Citext <$> do
      charList <- QuickCheck.listOf do
        QuickCheck.suchThat arbitrary (\char -> char /= '\NUL')
      pure (Text.pack charList)
  shrink (Citext base) =
    Citext . Text.pack <$> shrink (Text.unpack base)

instance IsScalar Citext where
  schemaName = Tagged Nothing
  typeName = Tagged "citext"
  baseOid = Tagged Nothing
  arrayOid = Tagged Nothing
  typeParams = Tagged []
  binaryEncoder (Citext base) = Write.textUtf8 base
  binaryDecoder = do
    bytes <- PtrPeeker.remainderAsByteString
    case Text.Encoding.decodeUtf8' bytes of
      Left e ->
        pure
          ( Left
              ( DecodingError
                  { location = [],
                    reason =
                      ParsingDecodingErrorReason
                        (fromString (show e))
                        bytes
                  }
              )
          )
      Right base -> pure (Right (Citext base))
  textualEncoder (Citext base) = TextBuilder.text base
  textualDecoder = Citext <$> Attoparsec.takeText

-- * Accessors

-- | Extract the underlying 'Data.Text.Text' value.
toText :: Citext -> Text.Text
toText (Citext t) = t

-- * Constructors

-- | Construct a PostgreSQL 'Citext' from a 'Data.Text.Text' value.
--
-- Strips null characters to ensure PostgreSQL compatibility.
normalizeFromText :: Text.Text -> Citext
normalizeFromText = Citext . Text.replace "\NUL" ""

-- | Construct a PostgreSQL 'Citext' from a 'Data.Text.Text' value.
--
-- Returns 'Nothing' if the text contains null characters (not supported by PostgreSQL).
refineFromText :: Text.Text -> Maybe Citext
refineFromText text =
  if Text.elem '\NUL' text
    then Nothing
    else Just (Citext text)