packages feed

pgmq-hasql-0.1.0.0: test/SchemaSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}

-- | Schema introspection tests
-- Verifies that pgmq.message_record type matches decoder expectations
module SchemaSpec (tests) where

import Data.Text (Text)
import Data.Vector qualified as V
import Hasql.Decoders qualified as D
import Hasql.Pool qualified as Pool
import Hasql.Session (Session, statement)
import Hasql.Statement (Statement, preparable)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertEqual, testCase)
import TestUtils (assertSession)

-- | All schema introspection tests
tests :: Pool.Pool -> TestTree
tests p =
  testGroup
    "Schema Introspection"
    [ testMessageRecordColumnOrder p,
      testMessageRecordColumnTypes p
    ]

-- | Expected column order for pgmq.message_record
-- This must match the order in Pgmq.Hasql.Decoders.messageDecoder
expectedMessageRecordColumns :: [(Text, Text)]
expectedMessageRecordColumns =
  [ ("msg_id", "int8"),
    ("read_ct", "int4"),
    ("enqueued_at", "timestamptz"),
    ("last_read_at", "timestamptz"),
    ("vt", "timestamptz"),
    ("message", "jsonb"),
    ("headers", "jsonb")
  ]

-- | Query to get column order of pgmq.message_record type
-- Returns columns in their attribute order
messageRecordColumnsQuery :: Statement () (V.Vector (Text, Text))
messageRecordColumnsQuery =
  preparable
    sql
    mempty
    decoder
  where
    sql =
      "SELECT a.attname::text, t.typname::text \
      \FROM pg_type typ \
      \JOIN pg_namespace ns ON typ.typnamespace = ns.oid \
      \JOIN pg_attribute a ON a.attrelid = typ.typrelid \
      \JOIN pg_type t ON a.atttypid = t.oid \
      \WHERE ns.nspname = 'pgmq' \
      \  AND typ.typname = 'message_record' \
      \  AND a.attnum > 0 \
      \ORDER BY a.attnum"

    decoder :: D.Result (V.Vector (Text, Text))
    decoder = D.rowVector $ (,) <$> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.text)

-- | Query columns of pgmq.message_record
getMessageRecordColumns :: Session (V.Vector (Text, Text))
getMessageRecordColumns = statement () messageRecordColumnsQuery

-- | Test that pgmq.message_record columns match expected order
-- This catches schema drift where columns are reordered or renamed
testMessageRecordColumnOrder :: Pool.Pool -> TestTree
testMessageRecordColumnOrder p = testCase "message_record column order matches decoder" $ do
  columns <- assertSession p getMessageRecordColumns
  let actualColumns = V.toList columns
      expectedColNames = map fst expectedMessageRecordColumns
      actualColNames = map fst actualColumns

  assertEqual
    "Column order must match decoder expectations (msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers)"
    expectedColNames
    actualColNames

-- | Test that pgmq.message_record column types match expected types
-- This catches type changes that would cause decoding failures
testMessageRecordColumnTypes :: Pool.Pool -> TestTree
testMessageRecordColumnTypes p = testCase "message_record column types match decoder" $ do
  columns <- assertSession p getMessageRecordColumns
  let actualColumns = V.toList columns

  assertEqual
    "Column types must match decoder expectations"
    expectedMessageRecordColumns
    actualColumns