packages feed

pgmq-config 0.6.1.1 → 0.7.0.0

raw patch · 37 files changed

+584/−99 lines, 37 filesdep +tasty-goldendep ~ephemeral-pgdep ~pgmq-configdep ~pgmq-corePVP ok

version bump matches the API change (PVP)

Dependencies added: tasty-golden

Dependency ranges changed: ephemeral-pg, pgmq-config, pgmq-core, pgmq-effectful, pgmq-hasql, pgmq-migration

API changes (from Hackage documentation)

+ Pgmq.Config: DetectedQueueNameCollision :: !QueueName -> !NonEmpty Text -> ReconcileAction
+ Pgmq.Config: UnsupportedNotifyOnPartitionedQueue :: !QueueName -> ReconcileAction
+ Pgmq.Config.Types: DetectedQueueNameCollision :: !QueueName -> !NonEmpty Text -> ReconcileAction
+ Pgmq.Config.Types: UnsupportedNotifyOnPartitionedQueue :: !QueueName -> ReconcileAction
+ Pgmq.Config.Types: instance Data.Aeson.Types.ToJSON.ToJSON Pgmq.Config.Types.ObservedQueueType
+ Pgmq.Config.Types: instance Data.Aeson.Types.ToJSON.ToJSON Pgmq.Config.Types.PartitionConfig
+ Pgmq.Config.Types: instance Data.Aeson.Types.ToJSON.ToJSON Pgmq.Config.Types.QueueType
+ Pgmq.Config.Types: instance Data.Aeson.Types.ToJSON.ToJSON Pgmq.Config.Types.ReconcileAction

Files

CHANGELOG.md view
@@ -1,5 +1,34 @@ # Revision history for pgmq-config +## 0.7.0.0 -- 2026-10-26++### Breaking Changes++- Add `DetectedQueueNameCollision QueueName (NonEmpty Text)` and+  `UnsupportedNotifyOnPartitionedQueue QueueName`, with matching JSON tags and+  fields. Exhaustive `ReconcileAction` matches must handle both constructors.++### New Features++- Add hand-written `ToJSON` instances for `ReconcileAction`, `QueueType`,+  `PartitionConfig` and `ObservedQueueType`, with an aeson `^>=2.2` library dependency.+  Reports use snake_case tags and field names; optional throttle/premake values+  encode as null, preserving declared defaults. Goldens cover every action and queue+  shape. The encoding is a published compatibility surface under design note 020;+  remove downstream orphan instances for these types.++### Bug Fixes++- Report foreign name collisions before creating missing aliases or sub-resources.+- Report partition notifications without enabling or updating unsupported resources.+  Drift still reconciles supported sub-resources against observed shape. Correct+  the guide's reconciliation and idempotence contract.++### Other Changes++- Ephemeral PostgreSQL roots are created/validated as owner-only (`0700`), existing+  owned roots are tightened, unsafe paths are rejected and registry contents survive.+ ## 0.6.1.1 -- 2026-09-25  Coordinated family version bump; the library is unchanged from 0.6.1.0.
pgmq-config.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: pgmq-config-version: 0.6.1.1+version: 0.7.0.0 synopsis:   Declarative queue configuration for PGMQ (PostgreSQL Message Queue) @@ -27,7 +27,9 @@ category: Database build-type: Simple extra-doc-files: CHANGELOG.md-extra-source-files: test/fixtures/pgmq-1.12.0.sql+extra-source-files:+  test/fixtures/pgmq-1.12.0.sql+  test/golden/*.json  flag effectful   description: Enable effectful integration@@ -64,21 +66,22 @@     OverloadedStrings    build-depends:+    aeson ^>=2.2,     base >=4.18 && <5,     containers >=0.6 && <0.8,     generic-lens ^>=2.2 || ^>=2.3,     hasql ^>=1.10,     hasql-pool ^>=1.4,     lens ^>=5.3,-    pgmq-core >=0.6 && <0.7,-    pgmq-hasql >=0.6 && <0.7,+    pgmq-core >=0.7 && <0.8,+    pgmq-hasql >=0.7 && <0.8,     text ^>=2.1,    if flag(effectful)     exposed-modules: Pgmq.Config.Effectful     build-depends:       effectful-core ^>=2.6 || ^>=2.7,-      pgmq-effectful >=0.6 && <0.7,+      pgmq-effectful >=0.7 && <0.8,   hs-source-dirs: src   default-language: GHC2024 @@ -94,11 +97,14 @@     -with-rtsopts=-N    other-modules:+    CollisionSpec     ConfigSpec     EphemeralDb     ForeignQueueSpec+    JsonGoldenSpec     NotifyCrashSpec     PartitionSpec+    RootPermissionsSpec    default-extensions:     DeriveGeneric@@ -114,19 +120,20 @@     base >=4.18 && <5,     bytestring >=0.11 && <0.13,     directory,-    ephemeral-pg >=0.3.1.0,+    ephemeral-pg >=0.3.1 && <0.4,     generic-lens ^>=2.2 || ^>=2.3,     hasql,     hasql-pool ^>=1.4,     lens ^>=5.3,     pg-migrate,-    pgmq-config >=0.6 && <0.7,-    pgmq-core >=0.6 && <0.7,-    pgmq-hasql >=0.6 && <0.7,-    pgmq-migration >=0.6 && <0.7,+    pgmq-config >=0.7 && <0.8,+    pgmq-core >=0.7 && <0.8,+    pgmq-hasql >=0.7 && <0.8,+    pgmq-migration >=0.7 && <0.8,     postgresql-libpq >=0.10.1 && <0.12,     random ^>=1.2,     tasty ^>=1.5,+    tasty-golden ^>=2.3.6,     tasty-hunit ^>=0.10,     text,     time ^>=1.14,@@ -139,4 +146,4 @@     cpp-options: -DPGMQ_EFFECTFUL     build-depends:       effectful-core ^>=2.6 || ^>=2.7,-      pgmq-effectful >=0.6 && <0.7,+      pgmq-effectful >=0.7 && <0.8,
src/Pgmq/Config.hs view
@@ -91,7 +91,9 @@ -- -- * A queue whose observed shape (standard, unlogged, or partitioned) --   contradicts the declared one is reported as---   'Pgmq.Config.Types.DetectedQueueTypeDrift' and nothing is mutated.+--   'Pgmq.Config.Types.DetectedQueueTypeDrift'. The queue's shape is not mutated;+--   its notify, FIFO-index and binding declarations are still reconciled against+--   its observed shape. --   Changing a queue's type means dropping and recreating it, destroying every --   message it holds; that is an operator's decision, not a startup task. --@@ -99,8 +101,19 @@ --   compared, because @pgmq.list_queues()@ does not report them. Only the --   three-way shape is checked. --+-- * Foreign mixed-case names colliding with the declared physical table are+--   reported as 'DetectedQueueNameCollision'. Without an exact registry row,+--   no queue or sub-resources are created. With an exact row, sub-resources proceed.+--   Detection uses the initial snapshot; it does not serialize foreign alias creation.+--+-- * Notifications on an observed or newly created partitioned queue are reported+--   as 'UnsupportedNotifyOnPartitionedQueue', with no enable, update or re-enable.+--   Existing throttle rows are left unchanged. See+--   @docs/design/015-notification-delivery-contract.md@.+-- -- Calling this on every application startup is the intended usage, and a second--- run against an unchanged config issues no mutations at all. One caveat about+-- run against unchanged config and unchanged, already-satisfied database state+-- issues no mutations. A later run restores crash-lost ordinary throttle rows. One caveat about -- /concurrent/ startups: queue creation and topic binding are serialized -- server-side and converge, but enabling insert notifications on a -- brand-new queue from two replicas at once can fail one of them with SQLSTATE@@ -133,7 +146,10 @@ -- order: exactly one queue-existence action per config -- ('Pgmq.Config.Types.CreatedQueue', 'Pgmq.Config.Types.SkippedQueue', or -- 'Pgmq.Config.Types.DetectedQueueTypeDrift'), then the notify, FIFO, and--- topic-binding actions for that config. Every @Skipped@ action means no+-- topic-binding actions for that config. A missing exact row with colliding names+-- instead reports only 'DetectedQueueNameCollision'; when an exact row exists,+-- that action follows the ordinary existence action and sub-resources proceed.+-- Every @Skipped@ action means no -- statement was issued. ensureQueuesReport :: [QueueConfig] -> Session [ReconcileAction] ensureQueuesReport = ensureQueuesReportWith sessionOps
src/Pgmq/Config/Reconcile.hs view
@@ -14,6 +14,8 @@ import Control.Lens ((^.)) import Data.Generics.Labels () import Data.Int (Int32)+import Data.List (sort)+import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map import Data.Maybe (fromMaybe) import Data.Set qualified as Set@@ -75,6 +77,7 @@    let existingQueuesByName =         Map.fromList [(q ^. #unvalidatedName, q) | q <- existingQueues]+      collisionsByLowerName = Map.fromListWith (++) [(T.toLower n, [n]) | n <- Map.keys existingQueuesByName]       existingBindingSet =         Set.fromList           [ (b ^. #bindingQueueName, topicPatternToText (b ^. #bindingPattern))@@ -92,7 +95,7 @@    concat     <$> traverse-      (reconcileQueue ops existingQueuesByName existingBindingSet existingNotifyByName existingFifoSet)+      (reconcileQueue ops existingQueuesByName collisionsByLowerName existingBindingSet existingNotifyByName existingFifoSet)       configs  -- | Reconcile a single queue config against existing state, returning actions taken.@@ -100,88 +103,97 @@   (Monad m) =>   ReconcileOps m ->   Map.Map T.Text UnvalidatedQueue ->+  Map.Map T.Text [T.Text] ->   Set.Set (T.Text, T.Text) ->   Map.Map T.Text Int32 ->   Set.Set T.Text ->   QueueConfig ->   m [ReconcileAction]-reconcileQueue ops existingQueues existingBindings existingNotify existingFifo cfg = do+reconcileQueue ops existingQueues collisionsByLowerName existingBindings existingNotify existingFifo cfg = do   let qn = cfg ^. #queueName       qnText = queueNameToText qn       declaredType = cfg ^. #queueType -  -- Queue creation, or — when the queue is already there — a shape comparison.-  -- Drift is reported and never repaired: the only way to change a queue's type-  -- is to drop and recreate it, which would destroy its messages.-  queueAction <--    case Map.lookup qnText existingQueues of-      Just observed ->-        let observedType = observedQueueType observed-         in pure-              [ if declaredShape declaredType == observedType-                  then SkippedQueue qn-                  else DetectedQueueTypeDrift qn declaredType observedType-              ]-      Nothing -> do-        case declaredType of-          StandardQueue ->-            (ops ^. #createQueue) qn-          UnloggedQueue ->-            (ops ^. #createUnloggedQueue) qn-          PartitionedQueue pc ->-            let query =-                  StmtTypes.CreatePartitionedQueue-                    { queueName = qn,-                      partitionInterval = pc ^. #partitionInterval,-                      retentionInterval = pc ^. #retentionInterval-                    }-             in case pc ^. #premake of-                  Nothing -> (ops ^. #createPartitionedQueue) query-                  Just n -> (ops ^. #createPartitionedQueueWithPremake) query n-        pure [CreatedQueue qn declaredType]+      colliding = NE.nonEmpty (sort (filter (/= qnText) (Map.findWithDefault [] qnText collisionsByLowerName)))+      collisionAction = maybe [] (\names -> [DetectedQueueNameCollision qn names]) colliding+      effectiveShape = maybe (declaredShape declaredType) observedQueueType (Map.lookup qnText existingQueues) -  -- Notification. A missing row is enabled; a row whose interval already-  -- matches is left strictly alone (re-enabling would reset last_notified_at);-  -- a row whose interval differs is updated in place.-  notifyAction <- case cfg ^. #notifyInsert of-    Nothing -> pure []-    Just nc ->-      let declaredMs = fromMaybe defaultThrottleMs (nc ^. #throttleMs)-       in case Map.lookup qnText existingNotify of-            Nothing -> do-              (ops ^. #enableNotifyInsert)-                StmtTypes.EnableNotifyInsert-                  { queueName = qn,-                    throttleIntervalMs = nc ^. #throttleMs-                  }-              pure [EnabledNotify qn (nc ^. #throttleMs)]-            Just observedMs-              | observedMs == declaredMs -> pure [SkippedNotify qn]-              | otherwise -> do-                  (ops ^. #updateNotifyInsert)-                    StmtTypes.UpdateNotifyInsert+  if Map.notMember qnText existingQueues && not (null collisionAction)+    then pure collisionAction+    else do+      -- Queue creation, or — when the queue is already there — a shape comparison.+      -- Drift is reported and never repaired: the only way to change a queue's type+      -- is to drop and recreate it, which would destroy its messages.+      queueAction <-+        case Map.lookup qnText existingQueues of+          Just observed ->+            let observedType = observedQueueType observed+             in pure+                  [ if declaredShape declaredType == observedType+                      then SkippedQueue qn+                      else DetectedQueueTypeDrift qn declaredType observedType+                  ]+          Nothing -> do+            case declaredType of+              StandardQueue ->+                (ops ^. #createQueue) qn+              UnloggedQueue ->+                (ops ^. #createUnloggedQueue) qn+              PartitionedQueue pc ->+                let query =+                      StmtTypes.CreatePartitionedQueue+                        { queueName = qn,+                          partitionInterval = pc ^. #partitionInterval,+                          retentionInterval = pc ^. #retentionInterval+                        }+                 in case pc ^. #premake of+                      Nothing -> (ops ^. #createPartitionedQueue) query+                      Just n -> (ops ^. #createPartitionedQueueWithPremake) query n+            pure [CreatedQueue qn declaredType]++      -- Notification. A missing row is enabled; a row whose interval already+      -- matches is left strictly alone (re-enabling would reset last_notified_at);+      -- a row whose interval differs is updated in place.+      notifyAction <- case cfg ^. #notifyInsert of+        Nothing -> pure []+        Just _ | effectiveShape == ObservedPartitioned -> pure [UnsupportedNotifyOnPartitionedQueue qn]+        Just nc ->+          let declaredMs = fromMaybe defaultThrottleMs (nc ^. #throttleMs)+           in case Map.lookup qnText existingNotify of+                Nothing -> do+                  (ops ^. #enableNotifyInsert)+                    StmtTypes.EnableNotifyInsert                       { queueName = qn,-                        throttleIntervalMs = declaredMs+                        throttleIntervalMs = nc ^. #throttleMs                       }-                  pure [UpdatedNotifyThrottle qn observedMs declaredMs]+                  pure [EnabledNotify qn (nc ^. #throttleMs)]+                Just observedMs+                  | observedMs == declaredMs -> pure [SkippedNotify qn]+                  | otherwise -> do+                      (ops ^. #updateNotifyInsert)+                        StmtTypes.UpdateNotifyInsert+                          { queueName = qn,+                            throttleIntervalMs = declaredMs+                          }+                      pure [UpdatedNotifyThrottle qn observedMs declaredMs] -  -- FIFO index. The catalog snapshot makes the report truthful; the underlying-  -- pgmq.create_fifo_index is CREATE INDEX IF NOT EXISTS, so losing a race with-  -- a concurrent replica degrades to a no-op rather than an error.-  fifoAction <--    if cfg ^. #fifoIndex-      then-        if Set.member qnText existingFifo-          then pure [SkippedFifoIndex qn]-          else do-            (ops ^. #createFifoIndex) qn-            pure [CreatedFifoIndex qn]-      else pure []+      -- FIFO index. The catalog snapshot makes the report truthful; the underlying+      -- pgmq.create_fifo_index is CREATE INDEX IF NOT EXISTS, so losing a race with+      -- a concurrent replica degrades to a no-op rather than an error.+      fifoAction <-+        if cfg ^. #fifoIndex+          then+            if Set.member qnText existingFifo+              then pure [SkippedFifoIndex qn]+              else do+                (ops ^. #createFifoIndex) qn+                pure [CreatedFifoIndex qn]+          else pure [] -  -- Topic bindings-  bindingActions <- concat <$> traverse (reconcileBinding ops qn qnText existingBindings) (cfg ^. #topicBindings)+      -- Topic bindings+      bindingActions <- concat <$> traverse (reconcileBinding ops qn qnText existingBindings) (cfg ^. #topicBindings) -  pure (queueAction ++ notifyAction ++ fifoAction ++ bindingActions)+      pure (queueAction ++ collisionAction ++ notifyAction ++ fifoAction ++ bindingActions)  -- | The shape a declared config asks for, reduced to what @pgmq.list_queues()@ -- can actually report. Partition interval, retention and premake are deliberately
src/Pgmq/Config/Types.hs view
@@ -29,9 +29,11 @@   ) where -import Control.Lens ((%~), (&))+import Control.Lens ((%~), (&), (^.))+import Data.Aeson (Encoding, KeyValue, Series, ToJSON (..), object, pairs, (.=)) import Data.Generics.Labels () import Data.Int (Int32)+import Data.List.NonEmpty (NonEmpty) import Data.Text (Text) import GHC.Generics (Generic) import Pgmq.Types (QueueName, TopicPattern)@@ -123,10 +125,11 @@   | -- | The queue exists but its observed shape contradicts the declared one.     -- Fields: queue, declared type, observed type.     ---    -- Nothing was mutated and nothing will be: converting a queue between-    -- standard, unlogged, and partitioned means dropping and recreating it,-    -- destroying every message it holds, which a startup reconciler must never-    -- do. Resolving the drift is an operator decision. This action replaces+    -- The queue itself is not mutated: conversion between standard, unlogged,+    -- and partitioned requires destroying its messages. Notification, FIFO-index+    -- and topic-binding declarations are still reconciled against the observed+    -- queue and each reports its own action. Resolving drift is an operator+    -- decision. This action replaces     -- 'SkippedQueue' for the queue it concerns, so the report still carries     -- exactly one queue-existence action per declared config.     --@@ -135,6 +138,21 @@     -- and retention settings, because @pgmq.list_queues()@ does not report     -- them.     DetectedQueueTypeDrift !QueueName !QueueType !ObservedQueueType+  | -- | Foreign registry names share the declared name's lowercased physical+    -- table. Fields: declared queue, every colliding registry name in ascending+    -- lexical order. Without an exact row this replaces 'CreatedQueue' and no+    -- notification, FIFO-index or binding declarations run. With an exact row,+    -- this follows the existence action and sub-resources proceed on that row.+    -- This action mutates nothing. Detection uses the initial snapshot; a foreign+    -- client can still create an alias afterward. Resolving aliases follows+    -- @docs/design/016-queue-name-validation.md@.+    DetectedQueueNameCollision !QueueName !(NonEmpty Text)+  | -- | Insert notifications were declared on an observed or newly created+    -- partitioned queue. Its cloned leaf triggers cannot deliver on+    -- 'Pgmq.Types.notifyChannelName'; see+    -- @docs/design/015-notification-delivery-contract.md@. Nothing was enabled,+    -- updated or re-enabled; an existing foreign throttle row is left unchanged.+    UnsupportedNotifyOnPartitionedQueue !QueueName   deriving stock (Show)  -- | The throttle interval pgmq applies when none is given: 250 milliseconds.@@ -181,3 +199,99 @@ withTopicBinding :: TopicPattern -> QueueConfig -> QueueConfig withTopicBinding pat cfg =   cfg & #topicBindings %~ (++ [pat])++-- JSON wire encodings ---------------------------------------------------------++encodeFields :: [Series] -> Encoding+encodeFields = pairs . mconcat++-- | Encodes as @{"type":"standard"}@, @{"type":"unlogged"}@, or+-- @{"type":"partitioned","partition_interval":…,"retention_interval":…,"premake":…}@.+-- See @docs/design/020-json-wire-encodings.md@.+queueTypeFields :: (KeyValue e kv) => QueueType -> [kv]+queueTypeFields qt = case qt of+  StandardQueue -> ["type" .= ("standard" :: Text)]+  UnloggedQueue -> ["type" .= ("unlogged" :: Text)]+  PartitionedQueue pc -> ("type" .= ("partitioned" :: Text)) : partitionConfigFields pc++-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.+instance ToJSON QueueType where+  toJSON = object . queueTypeFields+  toEncoding = encodeFields . queueTypeFields++-- | @premake@ is @null@ when creation uses the server default. See+-- @docs/design/020-json-wire-encodings.md@.+partitionConfigFields :: (KeyValue e kv) => PartitionConfig -> [kv]+partitionConfigFields pc =+  [ "partition_interval" .= (pc ^. #partitionInterval),+    "retention_interval" .= (pc ^. #retentionInterval),+    "premake" .= (pc ^. #premake)+  ]++-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.+instance ToJSON PartitionConfig where+  toJSON = object . partitionConfigFields+  toEncoding = encodeFields . partitionConfigFields++-- | Encodes as the bare strings @"standard"@, @"unlogged"@, @"partitioned"@.+observedQueueTypeText :: ObservedQueueType -> Text+observedQueueTypeText ot = case ot of+  ObservedStandard -> "standard"+  ObservedUnlogged -> "unlogged"+  ObservedPartitioned -> "partitioned"++-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.+instance ToJSON ObservedQueueType where+  toJSON = toJSON . observedQueueTypeText+  toEncoding = toEncoding . observedQueueTypeText++-- | A tagged object: @"action"@ holds the snake_case constructor name and the+-- remaining keys name the fields. @enabled_notify@'s @throttle_interval_ms@+-- is @null@ when the config declared the pgmq default ('defaultThrottleMs')+-- rather than a number; that is a different fact from declaring @250@.+-- Encode-only. The encoding is a published compatibility surface; see+-- @docs/design/020-json-wire-encodings.md@. Every constructor added later+-- must be encoded by the same rule and pinned in+-- @pgmq-config/test/JsonGoldenSpec.hs@.+reconcileActionFields :: (KeyValue e kv) => ReconcileAction -> [kv]+reconcileActionFields action = case action of+  CreatedQueue q t ->+    ["action" .= tag "created_queue", "queue" .= q, "queue_type" .= t]+  EnabledNotify q ms ->+    ["action" .= tag "enabled_notify", "queue" .= q, "throttle_interval_ms" .= ms]+  CreatedFifoIndex q ->+    ["action" .= tag "created_fifo_index", "queue" .= q]+  BoundTopic q p ->+    ["action" .= tag "bound_topic", "queue" .= q, "pattern" .= p]+  SkippedQueue q ->+    ["action" .= tag "skipped_queue", "queue" .= q]+  SkippedNotify q ->+    ["action" .= tag "skipped_notify", "queue" .= q]+  SkippedFifoIndex q ->+    ["action" .= tag "skipped_fifo_index", "queue" .= q]+  SkippedTopicBinding q p ->+    ["action" .= tag "skipped_topic_binding", "queue" .= q, "pattern" .= p]+  UpdatedNotifyThrottle q observed declared ->+    [ "action" .= tag "updated_notify_throttle",+      "queue" .= q,+      "observed_throttle_interval_ms" .= observed,+      "declared_throttle_interval_ms" .= declared+    ]+  DetectedQueueTypeDrift q declared observed ->+    [ "action" .= tag "detected_queue_type_drift",+      "queue" .= q,+      "declared" .= declared,+      "observed" .= observed+    ]+  DetectedQueueNameCollision q names ->+    ["action" .= tag "detected_queue_name_collision", "queue" .= q, "colliding_names" .= names]+  UnsupportedNotifyOnPartitionedQueue q ->+    ["action" .= tag "unsupported_notify_on_partitioned_queue", "queue" .= q]+  where+    tag :: Text -> Text+    tag = id++-- | JSON wire contract: @docs/design/020-json-wire-encodings.md@.+instance ToJSON ReconcileAction where+  toJSON = object . reconcileActionFields+  toEncoding = encodeFields . reconcileActionFields
+ test/CollisionSpec.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Schema-selectable isolated foreign-name fixtures for each backend.+module CollisionSpec (tests) where++import Control.Monad (forM_)+import Data.Foldable (toList)+import Data.List (sort)+import Data.Text (Text)+import Hasql.Pool qualified as Pool+import Hasql.Session qualified as Session+import PartitionSpec (backends, isolated, reconcile, right, session)+import Pgmq.Config+import Pgmq.Types (parseQueueName, parseTopicPattern)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertFailure, testCase, (@?=))++tests :: TestTree+tests =+  testGroup+    "CollisionSpec"+    [ testGroup+        (if backend then "effectful" else "direct")+        [ testCase "single alias blocks every declared sub-resource, including repeat" (collision backend ["Billing_Events"]),+          testCase "multiple aliases block every declared sub-resource, including repeat" (collision backend ["Billing_Events", "BILLING_EVENTS"]),+          testCase "exact row reports collision while reconciling sub-resources" (exactRow backend),+          testCase "noncolliding foreign name leaves declared queues unaffected" (noncolliding backend)+        ]+    | backend <- backends+    ]++collision :: Bool -> [Text] -> IO ()+collision backend names = isolated $ \pool -> do+  forM_ names $ \name -> sql pool ("SELECT pgmq.create('" <> name <> "')")+  q <- right (parseQueueName "billing_events")+  other <- right (parseQueueName "other")+  pat <- right (parseTopicPattern "billing.*")+  let config = withTopicBinding pat (withFifoIndex (withNotifyInsert (Just 0) (standardQueue q)))+  forM_ [1 :: Int, 2] $ \_ -> do+    actions <- right =<< reconcile backend pool [config, standardQueue other]+    case actions of+      [DetectedQueueNameCollision actual colliding, next] -> do+        actual @?= q+        sort (toList colliding) @?= sort names+        case next of+          CreatedQueue n _ -> n @?= other+          SkippedQueue n -> n @?= other+          unexpected -> assertFailure (show unexpected)+      unexpected -> assertFailure (show unexpected)+    sql pool ("DO $$ BEGIN IF (SELECT count(*) FROM pgmq.meta WHERE lower(queue_name)='billing_events') <> " <> (if length names == 1 then "1" else "2") <> " THEN RAISE EXCEPTION 'collision inserted metadata alias'; END IF; END $$")+    sql pool "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='billing_events') OR to_regclass('pgmq.q_billing_events_fifo_idx') IS NOT NULL OR EXISTS (SELECT FROM pgmq.topic_bindings WHERE queue_name='billing_events') THEN RAISE EXCEPTION 'collision mutated sub-resources'; END IF; IF NOT EXISTS (SELECT FROM pgmq.meta WHERE queue_name='other') THEN RAISE EXCEPTION 'unrelated queue blocked'; END IF; END $$"++sql :: Pool.Pool -> Text -> IO ()+sql pool command = session pool (Session.script command)++exactRow :: Bool -> IO ()+exactRow backend = isolated $ \pool -> do+  sql pool "SELECT pgmq.create('Billing_Events'); SELECT pgmq.create('BILLING_EVENTS'); SELECT pgmq.create('billing_events')"+  q <- right (parseQueueName "billing_events")+  other <- right (parseQueueName "other")+  pat <- right (parseTopicPattern "billing.*")+  let config = withTopicBinding pat (withFifoIndex (withNotifyInsert (Just 0) (standardQueue q)))+  actions <- right =<< reconcile backend pool [config, standardQueue other]+  case actions of+    [SkippedQueue n, DetectedQueueNameCollision c names, EnabledNotify e _, CreatedFifoIndex f, BoundTopic b _, CreatedQueue o _] -> do+      [n, c, e, f, b] @?= replicate 5 q+      o @?= other+      sort (toList names) @?= ["BILLING_EVENTS", "Billing_Events"]+    unexpected -> assertFailure (show unexpected)+  sql pool "DO $$ BEGIN IF (SELECT count(*) FROM pgmq.meta WHERE lower(queue_name)='billing_events') <> 3 OR NOT EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='billing_events') OR to_regclass('pgmq.q_billing_events_fifo_idx') IS NULL OR NOT EXISTS (SELECT FROM pgmq.topic_bindings WHERE queue_name='billing_events') THEN RAISE EXCEPTION 'exact row sub-resources missing'; END IF; END $$"++noncolliding :: Bool -> IO ()+noncolliding backend = isolated $ \pool -> do+  sql pool "SELECT pgmq.create('ForeignQueue')"+  q <- right (parseQueueName "billing_events")+  other <- right (parseQueueName "other")+  actions <- right =<< reconcile backend pool [standardQueue q, standardQueue other]+  case actions of+    [CreatedQueue n _, CreatedQueue o _] -> [n, o] @?= [q, other]+    unexpected -> assertFailure (show unexpected)
test/ConfigSpec.hs view
@@ -342,6 +342,8 @@ actionForQueue qn (SkippedTopicBinding q _) = q == qn actionForQueue qn (UpdatedNotifyThrottle q _ _) = q == qn actionForQueue qn (DetectedQueueTypeDrift q _ _) = q == qn+actionForQueue qn (DetectedQueueNameCollision q _) = q == qn+actionForQueue qn (UnsupportedNotifyOnPartitionedQueue q) = q == qn  -- | Silent-variant version of 'testEnsureQueuesIdempotent': call 'ensureQueues' -- twice with the same standard-queue config and confirm the queue exists exactly
test/EphemeralDb.hs view
@@ -5,13 +5,15 @@   ( -- * Database setup     withPgmqDb,     ephemeralConfig,+    ensureOwnerOnlyRoot,      -- * Re-exports     StartError,   ) where -import Control.Monad (filterM, when)+import Control.Monad (filterM, unless, when)+import Data.Bits ((.&.)) import Data.List.NonEmpty (NonEmpty (..)) import Data.Monoid (Last (..)) import Data.Text.IO qualified as TextIO@@ -32,8 +34,11 @@ import Hasql.Pool.Config qualified as PoolConfig import Hasql.Session qualified as Session import Pgmq.Migration qualified as Migration-import System.Directory (createDirectoryIfMissing, doesFileExist)+import System.Directory (doesFileExist) import System.Environment (lookupEnv)+import System.IO.Error (catchIOError, isAlreadyExistsError)+import System.Posix.Directory qualified as Posix+import System.Posix.Files (deviceID, fileID, fileMode, fileOwner, getSymbolicLinkStatus, isDirectory, setFileMode) import System.Posix.User (getEffectiveUserID)  -- | Root directory for ephemeral PostgreSQL clusters.@@ -51,11 +56,31 @@   uid <- getEffectiveUserID   pure ("/tmp/ephpg-pgmq-hs-" <> show uid) +-- | Establish a real effective-uid-owned root without deleting its contents.+ensureOwnerOnlyRoot :: FilePath -> IO ()+ensureOwnerOnlyRoot root = do+  catchIOError (Posix.createDirectory root 0o700) $ \err ->+    unless (isAlreadyExistsError err) (ioError err)+  uid <- getEffectiveUserID+  before <- getSymbolicLinkStatus root+  unless (isDirectory before && fileOwner before == uid) $+    ioError (userError ("Unsafe ephemeral PostgreSQL root: " <> root))+  setFileMode root 0o700+  after <- getSymbolicLinkStatus root+  unless+    ( isDirectory after+        && fileOwner after == uid+        && deviceID before == deviceID after+        && fileID before == fileID after+        && fileMode after .&. 0o7777 == 0o700+    )+    (ioError (userError ("Ephemeral PostgreSQL root changed or is not owner-only: " <> root)))+ -- | Cached-startup configuration pinned to 'ephemeralRoot'. ephemeralConfig :: IO Config ephemeralConfig = do   root <- ephemeralRoot-  createDirectoryIfMissing True root+  ensureOwnerOnlyRoot root   pure defaultConfig {temporaryRoot = Last (Just root)}  -- | Run an action with a temporary PostgreSQL database that has pgmq schema installed
+ test/JsonGoldenSpec.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE OverloadedStrings #-}++-- | One golden per 'ReconcileAction' constructor. A constructor without a+-- golden is a defect: the report is a published wire shape (see+-- @docs/design/020-json-wire-encodings.md@) once a consumer logs or serves it.+module JsonGoldenSpec (tests) where++import Data.Aeson (ToJSON (..), Value, decode, encode)+import Data.List.NonEmpty (NonEmpty (..))+import Pgmq.Config.Types+import Pgmq.Types (QueueName, TopicPattern, parseQueueName, parseTopicPattern)+import Test.Tasty (TestTree, localOption, testGroup)+import Test.Tasty.Golden (NoCreateFile (..), goldenVsString)+import Test.Tasty.HUnit (assertEqual, testCase)++tests :: TestTree+tests =+  localOption (NoCreateFile True) $+    testGroup+      "ReconcileAction wire encodings"+      [ golden "created_queue_standard" (CreatedQueue q StandardQueue),+        golden "created_queue_unlogged" (CreatedQueue q UnloggedQueue),+        golden "created_queue_partitioned" (CreatedQueue q (PartitionedQueue partition)),+        golden "created_queue_partitioned_default_premake" (CreatedQueue q (PartitionedQueue partition {premake = Nothing})),+        golden "enabled_notify" (EnabledNotify q (Just 1000)),+        golden "enabled_notify_default" (EnabledNotify q Nothing),+        golden "created_fifo_index" (CreatedFifoIndex q),+        golden "bound_topic" (BoundTopic q pat),+        golden "skipped_queue" (SkippedQueue q),+        golden "skipped_notify" (SkippedNotify q),+        golden "skipped_fifo_index" (SkippedFifoIndex q),+        golden "skipped_topic_binding" (SkippedTopicBinding q pat),+        golden "updated_notify_throttle" (UpdatedNotifyThrottle q 250 1000),+        golden "detected_queue_type_drift" (DetectedQueueTypeDrift q UnloggedQueue ObservedPartitioned),+        golden "detected_queue_name_collision" (DetectedQueueNameCollision q ("ORDERS" :| ["Orders"])),+        golden "unsupported_notify_on_partitioned_queue" (UnsupportedNotifyOnPartitionedQueue q),+        golden "queue_type_standard" StandardQueue,+        golden "queue_type_unlogged" UnloggedQueue,+        golden "queue_type_partitioned" (PartitionedQueue partition),+        golden "partition_config" partition,+        golden "partition_config_default" (partition {premake = Nothing}),+        golden "observed_standard" ObservedStandard,+        golden "observed_unlogged" ObservedUnlogged,+        golden "observed_partitioned" ObservedPartitioned+      ]+  where+    golden name value =+      testGroup+        name+        [ goldenVsString "bytes" ("test/golden/" <> name <> ".json") (pure (encode value <> "\n")),+          testCase "toJSON and toEncoding agree" $+            assertEqual "encoder parity" (Just (toJSON value)) (decode (encode value) :: Maybe Value)+        ]++q :: QueueName+q = either (error . show) id (parseQueueName "orders")++pat :: TopicPattern+pat = either (error . show) id (parseTopicPattern "orders.*")++partition :: PartitionConfig+partition = PartitionConfig {partitionInterval = "daily", retentionInterval = "7 days", premake = Just 2}
test/Main.hs view
@@ -2,11 +2,14 @@  module Main (main) where +import CollisionSpec qualified import ConfigSpec qualified-import EphemeralDb (withPgmqDb)+import EphemeralDb (ensureOwnerOnlyRoot, withPgmqDb) import ForeignQueueSpec qualified+import JsonGoldenSpec qualified import NotifyCrashSpec qualified import PartitionSpec qualified+import RootPermissionsSpec qualified import Test.Tasty (defaultMain, testGroup)  main :: IO ()@@ -15,8 +18,12 @@     let tree =           testGroup             "pgmq-config"-            [ ConfigSpec.tests pool,+            [ RootPermissionsSpec.tests ensureOwnerOnlyRoot,+              JsonGoldenSpec.tests,+              ConfigSpec.tests pool,               PartitionSpec.tests,+              -- Foreign names require an isolated, schema-selectable server.+              CollisionSpec.tests,               -- NotifyCrashSpec manages its own PostgreSQL instance: it crashes               -- the server, which the shared pool above could not survive.               NotifyCrashSpec.tests,
test/NotifyCrashSpec.hs view
@@ -35,6 +35,7 @@   ) import EphemeralDb (ephemeralConfig) import EphemeralPg qualified as Pg+ -- 'shutdownMode' names a field of both Pg.Config and Pg.Database, so the record -- update below needs the selector from the module that defines only Database. import EphemeralPg.Database qualified as PgDb@@ -70,7 +71,9 @@     -- | Channel of the notification delivered by the post-crash send, if any.     obsNotifyAfterCrash :: !(Maybe ByteString),     -- | Queue names in the throttle table after a reconcile ran.-    obsThrottlesAfterReconcile :: ![Text]+    obsThrottlesAfterReconcile :: ![Text],+    -- | Restored nonzero interval, without restarting the application.+    obsRestoredInterval :: !Int64   }  tests :: TestTree@@ -102,6 +105,7 @@         testCase "post-crash: a reconcile restores the throttle row" $ do           obs <- getObs           obsThrottlesAfterReconcile obs @?= [queueNameToText (obsQueueName obs)]+          obsRestoredInterval obs @?= 3600000       ]  -- | Start a dedicated PostgreSQL instance, enable notify, crash it, recover it,@@ -118,7 +122,7 @@   db0 <- readIORef ref   installPgmq db0 -  -- Pre-crash: create the queue, enable unthrottled notify, send one message.+  -- Pre-crash: create the queue, enable a nonzero throttle, send one message.   -- No listener is opened yet: an immediate shutdown kills every pre-crash   -- connection, so a listener created here could never see the assertion's   -- notification.@@ -128,7 +132,7 @@       Sessions.enableNotifyInsert         StmtTypes.EnableNotifyInsert           { StmtTypes.queueName = qn,-            StmtTypes.throttleIntervalMs = Just 0+            StmtTypes.throttleIntervalMs = Just 3600000           }     runSession pool (sendProbe qn "before-crash")     -- ephemeral-pg runs PostgreSQL with fsync, synchronous_commit and@@ -153,8 +157,9 @@       _ <- runSession pool (sendProbe qn "after-crash")       awaitNotify conn 20 -    runSession pool (ensureQueues [withNotifyInsert (Just 0) (standardQueue qn)])+    runSession pool (ensureQueues [withNotifyInsert (Just 3600000) (standardQueue qn)])     throttlesAfterReconcile <- listThrottleNames pool+    restored <- runSession pool (Session.statement (queueNameToText qn) (preparable "SELECT throttle_interval_ms::bigint FROM pgmq.notify_insert_throttle WHERE queue_name=$1" (E.param (E.nonNullable E.text)) (D.singleRow (D.column (D.nonNullable D.int8)))))      pure       CrashObservations@@ -164,7 +169,8 @@           obsTriggersAfterCrash = triggers,           obsQueueLengthAfterCrash = queueLength,           obsNotifyAfterCrash = LibPQ.notifyRelname <$> notified,-          obsThrottlesAfterReconcile = throttlesAfterReconcile+          obsThrottlesAfterReconcile = throttlesAfterReconcile,+          obsRestoredInterval = restored         }  -- | Stop PostgreSQL with SIGQUIT and start it again on the same data directory.@@ -297,7 +303,8 @@ awaitNotify conn attempts   | attempts <= 0 = pure Nothing   | otherwise = do-      _ <- LibPQ.consumeInput conn+      ok <- LibPQ.consumeInput conn+      unless ok (assertFailure "libpq consumeInput failed")       pending <- LibPQ.notifies conn       case pending of         Just n -> pure (Just n)
test/PartitionSpec.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE CPP #-} -module PartitionSpec (tests) where+module PartitionSpec (tests, backends, isolated, reconcile, right, session) where  import Control.Exception (finally) import Control.Monad (forM_)@@ -71,6 +71,43 @@               counts pool qn >>= (@?= [expected, expected])       | requested <- [Nothing, Just 2]       ]+      ++ [ testCase "unsupported partition notify installs no trigger or throttle" $ isolated $ \pool -> withPartman pool $ do+             qn <- right (parseQueueName "partition_notify")+             let config = withNotifyInsert (Just 0) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))+             first <- right =<< reconcile effectful pool [config]+             case first of+               [CreatedQueue q _, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]+               other -> assertFailure (show other)+             second <- right =<< reconcile effectful pool [config]+             case second of+               [SkippedQueue q, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]+               other -> assertFailure (show other)+             session pool (Session.script "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='partition_notify') OR EXISTS (SELECT FROM pg_trigger WHERE tgrelid='pgmq.q_partition_notify'::regclass AND tgname='trigger_notify_queue_insert_listeners') THEN RAISE EXCEPTION 'unsupported notification installed'; END IF; END $$"),+           testCase "unsupported existing partition throttle remains unchanged" $ isolated $ \pool -> withPartman pool $ do+             qn <- right (parseQueueName "partition_existing")+             session pool (Session.script "SELECT pgmq.create_partitioned('partition_existing','10','100'); SELECT pgmq.enable_notify_insert('partition_existing',250); CREATE TABLE public.before_throttle AS SELECT * FROM pgmq.notify_insert_throttle")+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 500) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))]+             case actions of+               [SkippedQueue q, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]+               other -> assertFailure (show other)+             session pool (Session.script "DO $$ BEGIN IF EXISTS ((TABLE public.before_throttle EXCEPT TABLE pgmq.notify_insert_throttle) UNION ALL (TABLE pgmq.notify_insert_throttle EXCEPT TABLE public.before_throttle)) THEN RAISE EXCEPTION 'partition throttle changed'; END IF; END $$"),+           testCase "observed partition declared standard reports drift and unsupported" $ isolated $ \pool -> withPartman pool $ do+             qn <- right (parseQueueName "partition_drift")+             session pool (Session.script "SELECT pgmq.create_partitioned('partition_drift','10','100')")+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 0) (standardQueue qn)]+             case actions of+               [DetectedQueueTypeDrift q StandardQueue ObservedPartitioned, UnsupportedNotifyOnPartitionedQueue n] -> [q, n] @?= [qn, qn]+               other -> assertFailure (show other)+             session pool (Session.script "DO $$ BEGIN IF EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='partition_drift') THEN RAISE EXCEPTION 'observed partition notification installed'; END IF; END $$"),+           testCase "observed standard declared partition reconciles notifications" $ isolated $ \pool -> do+             qn <- right (parseQueueName "standard_drift")+             session pool (Session.script "SELECT pgmq.create('standard_drift')")+             actions <- right =<< reconcile effectful pool [withNotifyInsert (Just 0) (partitionedQueue qn (PartitionConfig "10" "100" Nothing))]+             case actions of+               [DetectedQueueTypeDrift q (PartitionedQueue _) ObservedStandard, EnabledNotify n (Just 0)] -> [q, n] @?= [qn, qn]+               other -> assertFailure (show other)+             session pool (Session.script "DO $$ BEGIN IF NOT EXISTS (SELECT FROM pgmq.notify_insert_throttle WHERE queue_name='standard_drift' AND throttle_interval_ms=0) THEN RAISE EXCEPTION 'standard notification missing'; END IF; END $$")+         ]       ++ [ testCase "zero and negative counts fail on new queues" $ forM_ [0, -1] $ \n -> isolated $ \pool -> withPartman pool $ do              stock <- (== Just "1.12.0") <$> lookupEnv "PGMQ_TEST_SCHEMA_VERSION"              qn <- right (parseQueueName "invalid_partition_config")
+ test/RootPermissionsSpec.hs view
@@ -0,0 +1,63 @@+-- | Check each suite's local root helper using private disposable fixtures.+module RootPermissionsSpec (tests) where++import Control.Exception (IOException, bracket, try)+import Data.Bits ((.&.))+import System.Directory (getTemporaryDirectory, removePathForcibly)+import System.Posix.Directory qualified as Posix+import System.Posix.Files (createSymbolicLink, fileMode, getSymbolicLinkStatus, isDirectory, setFileMode)+import System.Posix.Temp (mkdtemp)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))++tests :: (FilePath -> IO ()) -> TestTree+tests ensure =+  testGroup+    "RootPermissionsSpec"+    [ testCase "missing root is a real 0700 directory" $ private $ \parent -> do+        let root = parent <> "/root"+        ensure root+        status <- getSymbolicLinkStatus root+        assertBool "real directory" (isDirectory status)+        fileMode status .&. 0o7777 @?= 0o700,+      testCase "owned 0755 directory is tightened without losing contents" $ private $ \parent -> do+        let root = parent <> "/root"+        Posix.createDirectory root 0o755+        setFileMode root 0o755+        writeFile (root <> "/sentinel") "preserve"+        ensure root+        mode root >>= (@?= 0o700)+        readFile (root <> "/sentinel") >>= (@?= "preserve"),+      testCase "repeated call preserves contents and permissions" $ private $ \parent -> do+        let root = parent <> "/root"+        ensure root+        writeFile (root <> "/sentinel") "preserve"+        ensure root+        mode root >>= (@?= 0o700)+        readFile (root <> "/sentinel") >>= (@?= "preserve"),+      testCase "regular file is rejected unchanged" $ private $ \parent -> do+        let root = parent <> "/root"+        writeFile root "preserve"+        setFileMode root 0o644+        rejected (ensure root)+        mode root >>= (@?= 0o644)+        readFile root >>= (@?= "preserve"),+      testCase "symlink is rejected without chmod of its target" $ private $ \parent -> do+        let root = parent <> "/root"+            target = parent <> "/target"+        Posix.createDirectory target 0o755+        setFileMode target 0o755+        writeFile (target <> "/sentinel") "preserve"+        createSymbolicLink target root+        rejected (ensure root)+        mode target >>= (@?= 0o755)+        readFile (target <> "/sentinel") >>= (@?= "preserve")+    ]+  where+    mode path = (.&. 0o7777) . fileMode <$> getSymbolicLinkStatus path+    rejected action = do+      result <- try action :: IO (Either IOException ())+      case result of+        Left _ -> pure ()+        Right () -> assertFailure "unsafe root accepted"+    private action = bracket (getTemporaryDirectory >>= \tmp -> mkdtemp (tmp <> "/pgmq-root-test-")) removePathForcibly action
+ test/golden/bound_topic.json view
@@ -0,0 +1,1 @@+{"action":"bound_topic","queue":"orders","pattern":"orders.*"}
+ test/golden/created_fifo_index.json view
@@ -0,0 +1,1 @@+{"action":"created_fifo_index","queue":"orders"}
+ test/golden/created_queue_partitioned.json view
@@ -0,0 +1,1 @@+{"action":"created_queue","queue":"orders","queue_type":{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":2}}
+ test/golden/created_queue_partitioned_default_premake.json view
@@ -0,0 +1,1 @@+{"action":"created_queue","queue":"orders","queue_type":{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":null}}
+ test/golden/created_queue_standard.json view
@@ -0,0 +1,1 @@+{"action":"created_queue","queue":"orders","queue_type":{"type":"standard"}}
+ test/golden/created_queue_unlogged.json view
@@ -0,0 +1,1 @@+{"action":"created_queue","queue":"orders","queue_type":{"type":"unlogged"}}
+ test/golden/detected_queue_name_collision.json view
@@ -0,0 +1,1 @@+{"action":"detected_queue_name_collision","queue":"orders","colliding_names":["ORDERS","Orders"]}
+ test/golden/detected_queue_type_drift.json view
@@ -0,0 +1,1 @@+{"action":"detected_queue_type_drift","queue":"orders","declared":{"type":"unlogged"},"observed":"partitioned"}
+ test/golden/enabled_notify.json view
@@ -0,0 +1,1 @@+{"action":"enabled_notify","queue":"orders","throttle_interval_ms":1000}
+ test/golden/enabled_notify_default.json view
@@ -0,0 +1,1 @@+{"action":"enabled_notify","queue":"orders","throttle_interval_ms":null}
+ test/golden/observed_partitioned.json view
@@ -0,0 +1,1 @@+"partitioned"
+ test/golden/observed_standard.json view
@@ -0,0 +1,1 @@+"standard"
+ test/golden/observed_unlogged.json view
@@ -0,0 +1,1 @@+"unlogged"
+ test/golden/partition_config.json view
@@ -0,0 +1,1 @@+{"partition_interval":"daily","retention_interval":"7 days","premake":2}
+ test/golden/partition_config_default.json view
@@ -0,0 +1,1 @@+{"partition_interval":"daily","retention_interval":"7 days","premake":null}
+ test/golden/queue_type_partitioned.json view
@@ -0,0 +1,1 @@+{"type":"partitioned","partition_interval":"daily","retention_interval":"7 days","premake":2}
+ test/golden/queue_type_standard.json view
@@ -0,0 +1,1 @@+{"type":"standard"}
+ test/golden/queue_type_unlogged.json view
@@ -0,0 +1,1 @@+{"type":"unlogged"}
+ test/golden/skipped_fifo_index.json view
@@ -0,0 +1,1 @@+{"action":"skipped_fifo_index","queue":"orders"}
+ test/golden/skipped_notify.json view
@@ -0,0 +1,1 @@+{"action":"skipped_notify","queue":"orders"}
+ test/golden/skipped_queue.json view
@@ -0,0 +1,1 @@+{"action":"skipped_queue","queue":"orders"}
+ test/golden/skipped_topic_binding.json view
@@ -0,0 +1,1 @@+{"action":"skipped_topic_binding","queue":"orders","pattern":"orders.*"}
+ test/golden/unsupported_notify_on_partitioned_queue.json view
@@ -0,0 +1,1 @@+{"action":"unsupported_notify_on_partitioned_queue","queue":"orders"}
+ test/golden/updated_notify_throttle.json view
@@ -0,0 +1,1 @@+{"action":"updated_notify_throttle","queue":"orders","observed_throttle_interval_ms":250,"declared_throttle_interval_ms":1000}