pgmq-migration 0.1.1.0 → 0.1.2.0
raw patch · 48 files changed
+3265/−8133 lines, 48 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Pgmq.Migration.Migrations.V1_10_0: migrations :: [MigrationCommand]
- Pgmq.Migration.Migrations.V1_10_0: version :: String
- Pgmq.Migration.Migrations.V1_10_0_to_V1_11_0: migrations :: [MigrationCommand]
- Pgmq.Migration.Migrations.V1_9_0: migrations :: [MigrationCommand]
- Pgmq.Migration.Migrations.V1_9_0: version :: String
- Pgmq.Migration.Migrations.V1_9_0_to_V1_10_0: migrations :: [MigrationCommand]
+ Pgmq.Migration.Migrations.V1_10_0_to_V1_10_1: migrations :: [MigrationCommand]
+ Pgmq.Migration.Migrations.V1_10_1_to_V1_11_0: migrations :: [MigrationCommand]
Files
- CHANGELOG.md +7/−0
- database/migrations/v1.10.0_to_v1.11.0.sql +0/−474
- database/migrations/v1.9.0_to_v1.10.0.sql +0/−594
- database/v1.10.0/01_schema.sql +0/−11
- database/v1.10.0/02_tables.sql +0/−25
- database/v1.10.0/03_types.sql +0/−34
- database/v1.10.0/04_core_functions.sql +0/−69
- database/v1.10.0/05_queue_management.sql +0/−265
- database/v1.10.0/06_message_ops.sql +0/−676
- database/v1.10.0/07_metrics.sql +0/−83
- database/v1.10.0/08_partitioning.sql +0/−268
- database/v1.10.0/09_notifications.sql +0/−103
- database/v1.10.0/10_fifo.sql +0/−571
- database/v1.11.0/01_schema.sql +0/−11
- database/v1.11.0/02_tables.sql +0/−49
- database/v1.11.0/03_types.sql +0/−34
- database/v1.11.0/04_core_functions.sql +0/−69
- database/v1.11.0/05_queue_management.sql +0/−265
- database/v1.11.0/06_message_ops.sql +0/−676
- database/v1.11.0/07_metrics.sql +0/−83
- database/v1.11.0/08_partitioning.sql +0/−268
- database/v1.11.0/09_notifications.sql +0/−167
- database/v1.11.0/10_fifo.sql +0/−571
- database/v1.11.0/11_topics.sql +0/−395
- database/v1.9.0/01_schema.sql +0/−11
- database/v1.9.0/02_tables.sql +0/−25
- database/v1.9.0/03_types.sql +0/−32
- database/v1.9.0/04_core_functions.sql +0/−69
- database/v1.9.0/05_queue_management.sql +0/−261
- database/v1.9.0/06_message_ops.sql +0/−626
- database/v1.9.0/07_metrics.sql +0/−83
- database/v1.9.0/08_partitioning.sql +0/−263
- database/v1.9.0/09_notifications.sql +0/−103
- database/v1.9.0/10_fifo.sql +0/−564
- pgmq-migration.cabal +7/−6
- src/Pgmq/Migration.hs +3/−3
- src/Pgmq/Migration/Migrations.hs +17/−7
- src/Pgmq/Migration/Migrations/V1_10_0.hs +0/−63
- src/Pgmq/Migration/Migrations/V1_10_0_to_V1_10_1.hs +21/−0
- src/Pgmq/Migration/Migrations/V1_10_0_to_V1_11_0.hs +0/−22
- src/Pgmq/Migration/Migrations/V1_10_1_to_V1_11_0.hs +21/−0
- src/Pgmq/Migration/Migrations/V1_11_0.hs +8/−46
- src/Pgmq/Migration/Migrations/V1_9_0.hs +0/−62
- src/Pgmq/Migration/Migrations/V1_9_0_to_V1_10_0.hs +0/−21
- test/Main.hs +23/−105
- vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql +291/−0
- vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql +791/−0
- vendor/pgmq/pgmq-extension/sql/pgmq.sql +2076/−0
CHANGELOG.md view
@@ -1,5 +1,12 @@ # Changelog for pgmq-migration +## 0.1.2.0 -- 2026-03-03++### Other Changes++* Vendor upstream pgmq SQL via git subtree, replacing hand-written SQL files+* SQL is now embedded from `vendor/pgmq/pgmq-extension/sql/` instead of local `database/` directory+ ## 0.1.1.0 -- 2026-02-23 ### New Features
− database/migrations/v1.10.0_to_v1.11.0.sql
@@ -1,474 +0,0 @@---------------------------------------------------------------- Upgrade migration: pgmq v1.10.0 -> v1.11.0--- Combines changes from v1.10.0->v1.10.1 and v1.10.1->v1.11.0----------------------------------------------------------------- 1. Topic bindings table-CREATE TABLE IF NOT EXISTS pgmq.topic_bindings-(- pattern text NOT NULL,- queue_name text NOT NULL- CONSTRAINT topic_bindings_meta_queue_name_fk- REFERENCES pgmq.meta (queue_name)- ON DELETE CASCADE,- bound_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- compiled_regex text GENERATED ALWAYS AS (- '^' ||- replace(- replace(- regexp_replace(pattern, '([.+?{}()|\[\]\\^$])', '\\\1', 'g'),- '*', '[^.]+'- ),- '#', '.*'- ) || '$'- ) STORED,- CONSTRAINT topic_bindings_unique_pattern_queue UNIQUE (pattern, queue_name)-);--CREATE INDEX IF NOT EXISTS idx_topic_bindings_covering ON pgmq.topic_bindings (pattern) INCLUDE (queue_name, compiled_regex);---- 2. Topic validation functions-CREATE OR REPLACE FUNCTION pgmq.validate_routing_key(routing_key text)- RETURNS boolean- LANGUAGE plpgsql- IMMUTABLE-AS-$$-BEGIN- IF routing_key IS NULL OR routing_key = '' THEN- RAISE EXCEPTION 'routing_key cannot be NULL or empty';- END IF;-- IF length(routing_key) > 255 THEN- RAISE EXCEPTION 'routing_key length cannot exceed 255 characters, got % characters', length(routing_key);- END IF;-- IF routing_key !~ '^[a-zA-Z0-9._-]+$' THEN- RAISE EXCEPTION 'routing_key contains invalid characters. Only alphanumeric, dots, hyphens, and underscores are allowed. Got: %', routing_key;- END IF;-- IF routing_key ~ '^\.' THEN- RAISE EXCEPTION 'routing_key cannot start with a dot. Got: %', routing_key;- END IF;-- IF routing_key ~ '\.$' THEN- RAISE EXCEPTION 'routing_key cannot end with a dot. Got: %', routing_key;- END IF;-- IF routing_key ~ '\.\.' THEN- RAISE EXCEPTION 'routing_key cannot contain consecutive dots. Got: %', routing_key;- END IF;-- RETURN true;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.validate_topic_pattern(pattern text)- RETURNS boolean- LANGUAGE plpgsql- IMMUTABLE-AS-$$-BEGIN- IF pattern IS NULL OR pattern = '' THEN- RAISE EXCEPTION 'pattern cannot be NULL or empty';- END IF;-- IF length(pattern) > 255 THEN- RAISE EXCEPTION 'pattern length cannot exceed 255 characters, got % characters', length(pattern);- END IF;-- IF pattern !~ '^[a-zA-Z0-9._\-*#]+$' THEN- RAISE EXCEPTION 'pattern contains invalid characters. Only alphanumeric, dots, hyphens, underscores, *, and # are allowed. Got: %', pattern;- END IF;-- IF pattern ~ '^\.' THEN- RAISE EXCEPTION 'pattern cannot start with a dot. Got: %', pattern;- END IF;-- IF pattern ~ '\.$' THEN- RAISE EXCEPTION 'pattern cannot end with a dot. Got: %', pattern;- END IF;-- IF pattern ~ '\.\.' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive dots. Got: %', pattern;- END IF;-- IF pattern ~ '\*\*' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive stars (**). Use # for multi-segment matching. Got: %', pattern;- END IF;-- IF pattern ~ '##' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive hashes (##). A single # already matches zero or more segments. Got: %', pattern;- END IF;-- IF pattern ~ '\*#' OR pattern ~ '#\*' THEN- RAISE EXCEPTION 'pattern cannot contain adjacent wildcards (*# or #*). Separate wildcards with dots. Got: %', pattern;- END IF;-- RETURN true;-END;-$$;---- 3. Topic binding management functions-CREATE OR REPLACE FUNCTION pgmq.bind_topic(pattern text, queue_name text)- RETURNS void- LANGUAGE plpgsql-AS-$$-BEGIN- PERFORM pgmq.validate_topic_pattern(pattern);- IF queue_name IS NULL OR queue_name = '' THEN- RAISE EXCEPTION 'queue_name cannot be NULL or empty';- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = bind_topic.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;- END IF;-- INSERT INTO pgmq.topic_bindings (pattern, queue_name)- VALUES (pattern, queue_name)- ON CONFLICT ON CONSTRAINT topic_bindings_unique_pattern_queue DO NOTHING;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.unbind_topic(pattern text, queue_name text)- RETURNS boolean- LANGUAGE plpgsql-AS-$$-DECLARE- rows_deleted integer;-BEGIN- IF pattern IS NULL OR pattern = '' THEN- RAISE EXCEPTION 'pattern cannot be NULL or empty';- END IF;-- IF queue_name IS NULL OR queue_name = '' THEN- RAISE EXCEPTION 'queue_name cannot be NULL or empty';- END IF;-- DELETE- FROM pgmq.topic_bindings- WHERE topic_bindings.pattern = unbind_topic.pattern- AND topic_bindings.queue_name = unbind_topic.queue_name;-- GET DIAGNOSTICS rows_deleted = ROW_COUNT;-- IF rows_deleted > 0 THEN- RETURN true;- ELSE- RETURN false;- END IF;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.test_routing(routing_key text)- RETURNS TABLE- (- pattern text,- queue_name text,- compiled_regex text- )- LANGUAGE plpgsql- STABLE-AS-$$-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);- RETURN QUERY- SELECT b.pattern,- b.queue_name,- b.compiled_regex- FROM pgmq.topic_bindings b- WHERE routing_key ~ b.compiled_regex- ORDER BY b.pattern;-END;-$$;---- 4. Topic send functions-CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, headers jsonb, delay integer)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-DECLARE- b RECORD;- matched_count integer := 0;-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);-- IF msg IS NULL THEN- RAISE EXCEPTION 'msg cannot be NULL';- END IF;-- IF delay < 0 THEN- RAISE EXCEPTION 'delay cannot be negative, got: %', delay;- END IF;-- FOR b IN- SELECT DISTINCT tb.queue_name- FROM pgmq.topic_bindings tb- WHERE routing_key ~ tb.compiled_regex- ORDER BY tb.queue_name- LOOP- PERFORM pgmq.send(b.queue_name, msg, headers, delay);- matched_count := matched_count + 1;- END LOOP;-- RETURN matched_count;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-BEGIN- RETURN pgmq.send_topic(routing_key, msg, NULL, 0);-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, delay integer)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-BEGIN- RETURN pgmq.send_topic(routing_key, msg, NULL, delay);-END;-$$;---- 5. Topic listing functions-CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings()- RETURNS TABLE- (- pattern text,- queue_name text,- bound_at TIMESTAMP WITH TIME ZONE,- compiled_regex text- )- LANGUAGE sql- STABLE-AS-$$- SELECT pattern, queue_name, bound_at, compiled_regex- FROM pgmq.topic_bindings- ORDER BY bound_at DESC, pattern, queue_name;-$$;--CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings(queue_name text)- RETURNS TABLE- (- pattern text,- queue_name text,- bound_at TIMESTAMP WITH TIME ZONE,- compiled_regex text- )- LANGUAGE sql- STABLE-AS-$$- SELECT pattern, tb.queue_name, bound_at, compiled_regex- FROM pgmq.topic_bindings tb- WHERE tb.queue_name = list_topic_bindings.queue_name- ORDER BY bound_at DESC, pattern;-$$;---- 6. Notification management functions-CREATE OR REPLACE FUNCTION pgmq.list_notify_insert_throttles()- RETURNS TABLE- (- queue_name text,- throttle_interval_ms integer,- last_notified_at TIMESTAMP WITH TIME ZONE- )- LANGUAGE sql- STABLE-AS-$$- SELECT queue_name, throttle_interval_ms, last_notified_at- FROM pgmq.notify_insert_throttle- ORDER BY queue_name;-$$;--CREATE OR REPLACE FUNCTION pgmq.update_notify_insert(queue_name text, throttle_interval_ms integer)- RETURNS void- LANGUAGE plpgsql-AS-$$-BEGIN- IF throttle_interval_ms < 0 THEN- RAISE EXCEPTION 'throttle_interval_ms must be non-negative, got: %', throttle_interval_ms;- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = update_notify_insert.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.notify_insert_throttle WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not have notify_insert enabled. Enable it first using pgmq.enable_notify_insert()', queue_name;- END IF;-- UPDATE pgmq.notify_insert_throttle- SET throttle_interval_ms = update_notify_insert.throttle_interval_ms,- last_notified_at = to_timestamp(0)- WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name;-END;-$$;---- 7. Batch validation helpers-CREATE OR REPLACE FUNCTION pgmq._validate_batch_params(- msgs JSONB[],- headers JSONB[]-) RETURNS void AS $$-BEGIN- IF msgs IS NULL OR array_length(msgs, 1) IS NULL THEN- RAISE EXCEPTION 'msgs cannot be NULL or empty';- END IF;-- IF headers IS NOT NULL AND COALESCE(array_length(headers, 1), 0) != COALESCE(array_length(msgs, 1), 0) THEN- RAISE EXCEPTION 'headers array length (%) must match msgs array length (%)',- COALESCE(array_length(headers, 1), 0), COALESCE(array_length(msgs, 1), 0);- END IF;-END;-$$ LANGUAGE plpgsql;--CREATE OR REPLACE FUNCTION pgmq._send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay TIMESTAMP WITH TIME ZONE-) RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- SELECT $2, unnest($1), unnest(coalesce($3, ARRAY[]::jsonb[]))- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING msgs, delay, headers;-END;-$$ LANGUAGE plpgsql;---- Update send_batch to use validation--- Must drop first because parameter name changed from 'vt' to 'delay'-DROP FUNCTION IF EXISTS pgmq.send_batch(text, jsonb[], jsonb[], timestamp with time zone);-CREATE OR REPLACE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay TIMESTAMP WITH TIME ZONE-) RETURNS SETOF BIGINT AS $$-BEGIN- PERFORM pgmq._validate_batch_params(msgs, headers);- RETURN QUERY SELECT * FROM pgmq._send_batch(queue_name, msgs, headers, delay);-END;-$$ LANGUAGE plpgsql;---- 8. Batch topic send functions-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[],- delay TIMESTAMP WITH TIME ZONE-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE plpgsql- VOLATILE-AS-$$-DECLARE- b RECORD;-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);- PERFORM pgmq._validate_batch_params(msgs, headers);-- FOR b IN- SELECT DISTINCT tb.queue_name- FROM pgmq.topic_bindings tb- WHERE routing_key ~ tb.compiled_regex- ORDER BY tb.queue_name- LOOP- RETURN QUERY- SELECT b.queue_name, batch_result.msg_id- FROM pgmq._send_batch(b.queue_name, msgs, headers, delay) AS batch_result(msg_id);- END LOOP;-- RETURN;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[]-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp());-$$;--CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[]-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp());-$$;--CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- delay integer-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp() + make_interval(secs => delay));-$$;--CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- delay TIMESTAMP WITH TIME ZONE-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, delay);-$$;--CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[],- delay integer-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp() + make_interval(secs => delay));-$$;
− database/migrations/v1.9.0_to_v1.10.0.sql
@@ -1,594 +0,0 @@--- Add last_read_at column to all existing queue tables (q_*) and archive tables (a_*)-DO $$-DECLARE- queue_record RECORD;- qtable TEXT;- atable TEXT;-BEGIN- FOR queue_record IN SELECT queue_name FROM pgmq.meta LOOP- qtable := 'q_' || queue_record.queue_name;- atable := 'a_' || queue_record.queue_name;-- -- Add last_read_at to queue table if it doesn't exist- IF NOT EXISTS (- SELECT 1 FROM information_schema.columns- WHERE table_schema = 'pgmq'- AND table_name = qtable- AND column_name = 'last_read_at'- ) THEN- EXECUTE FORMAT('ALTER TABLE pgmq.%I ADD COLUMN last_read_at TIMESTAMP WITH TIME ZONE', qtable);- END IF;-- -- Add last_read_at to archive table if it doesn't exist- IF EXISTS (- SELECT 1 FROM information_schema.tables- WHERE table_schema = 'pgmq'- AND table_name = atable- ) AND NOT EXISTS (- SELECT 1 FROM information_schema.columns- WHERE table_schema = 'pgmq'- AND table_name = atable- AND column_name = 'last_read_at'- ) THEN- EXECUTE FORMAT('ALTER TABLE pgmq.%I ADD COLUMN last_read_at TIMESTAMP WITH TIME ZONE', atable);- END IF;- END LOOP;-END;-$$;---- The functions that use this type are being dropped and recreated below-DROP TYPE IF EXISTS pgmq.message_record CASCADE;--CREATE TYPE pgmq.message_record AS (- msg_id BIGINT,- read_ct INTEGER,- enqueued_at TIMESTAMP WITH TIME ZONE,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE,- message JSONB,- headers JSONB-);----------------------------------------------------------------- Migration: Updated functions---------------------------------------------------------------DROP FUNCTION IF EXISTS pgmq.read_grouped_rr(-queue_name TEXT,- vt INTEGER,- qty INTEGER-);-CREATE FUNCTION pgmq.read_grouped_rr(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Determine the absolute head (oldest) message id per FIFO group, regardless of visibility- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') AS fifo_key,- MIN(msg_id) AS head_msg_id- FROM pgmq.%1$I- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- eligible_groups AS (- -- Only groups whose head message is currently visible- -- Acquire a transaction-level advisory lock per group to prevent concurrent selection- SELECT- g.fifo_key,- g.head_msg_id,- ROW_NUMBER() OVER (ORDER BY g.head_msg_id) AS group_priority- FROM fifo_groups g- JOIN pgmq.%2$I h ON h.msg_id = g.head_msg_id- WHERE h.vt <= clock_timestamp()- AND pg_try_advisory_xact_lock(pg_catalog.hashtextextended(g.fifo_key, 0))- ),- available_messages AS (- -- All currently visible messages starting at the head for each eligible group- SELECT- m.msg_id,- eg.group_priority,- ROW_NUMBER() OVER (- PARTITION BY eg.fifo_key- ORDER BY m.msg_id- ) AS msg_rank_in_group- FROM pgmq.%3$I m- JOIN eligible_groups eg- ON COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = eg.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= eg.head_msg_id- ),- ordered_messages AS (- -- Layered round-robin: take rank 1 of all groups by group_priority, then rank 2, etc.- -- Assign selection order before locking- SELECT msg_id, ROW_NUMBER() OVER (ORDER BY msg_rank_in_group, group_priority) as selection_order- FROM available_messages- ),- selected_messages AS (- -- Lock the messages in the correct order, preserving selection_order- SELECT om.msg_id, om.selection_order- FROM ordered_messages om- JOIN pgmq.%4$I m ON m.msg_id = om.msg_id- WHERE om.selection_order <= $1- ORDER BY om.selection_order- FOR UPDATE OF m SKIP LOCKED- ),- updated_messages AS (- UPDATE pgmq.%5$I m- SET- vt = clock_timestamp() + %6$L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- AND m.vt <= clock_timestamp() -- final guard to avoid duplicate reads under races- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers, sm.selection_order- )- SELECT msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers- FROM updated_messages- ORDER BY selection_order;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---DROP FUNCTION IF EXISTS pgmq.read_grouped_rr_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER-);-CREATE FUNCTION pgmq.read_grouped_rr_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped_rr(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---DROP FUNCTION IF EXISTS pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-);-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB DEFAULT '{}'-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND CASE- WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer- ELSE 1- END = 1- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- last_read_at = clock_timestamp(),- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.*;- $QUERY$,- qtable, conditional, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;--DROP FUNCTION IF EXISTS pgmq.read_grouped(- queue_name TEXT,- vt INTEGER,- qty INTEGER-);-CREATE FUNCTION pgmq.read_grouped(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Find the minimum msg_id for each FIFO group that's ready to be processed- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,- MIN(msg_id) as min_msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- locked_groups AS (- -- Lock the first available message in each FIFO group- SELECT- m.msg_id,- fg.fifo_key- FROM pgmq.%I m- INNER JOIN fifo_groups fg ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key- AND m.msg_id = fg.min_msg_id- WHERE m.vt <= clock_timestamp()- ORDER BY m.msg_id ASC- FOR UPDATE SKIP LOCKED- ),- group_priorities AS (- -- Assign priority to groups based on their oldest message- SELECT- fifo_key,- msg_id as min_msg_id,- ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority- FROM locked_groups- ),- available_messages AS (- -- Get messages prioritizing filling batch from earliest group first- SELECT- m.msg_id,- gp.group_priority,- ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY m.msg_id) as msg_rank_in_group- FROM pgmq.%I m- INNER JOIN group_priorities gp ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= gp.min_msg_id -- Only messages from min_msg_id onwards in each group- AND NOT EXISTS (- -- Ensure no earlier message in this group is currently being processed- SELECT 1- FROM pgmq.%I m2- WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') =- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group')- AND m2.vt > clock_timestamp()- AND m2.msg_id < m.msg_id- )- ),- batch_selection AS (- -- Select messages to fill batch, prioritizing earliest group- SELECT- msg_id,- ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank- FROM available_messages- ),- selected_messages AS (- -- Limit to requested quantity- SELECT msg_id- FROM batch_selection- WHERE overall_rank <= $1- ORDER BY msg_id- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---DROP FUNCTION IF EXISTS pgmq.read_grouped_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER-);-CREATE FUNCTION pgmq.read_grouped_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;--DROP FUNCTION IF EXISTS pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER,- conditional JSONB-);-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100,- conditional JSONB DEFAULT '{}'-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND CASE- WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer- ELSE 1- END = 1- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- last_read_at = clock_timestamp(),- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.*;- $QUERY$,- qtable, conditional, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- Update archive functions to include last_read_at column-CREATE OR REPLACE FUNCTION pgmq.archive(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;--CREATE OR REPLACE FUNCTION pgmq.archive(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;--DROP FUNCTION IF EXISTS pgmq.pop(- queue_name TEXT,- qty INTEGER-);-CREATE FUNCTION pgmq.pop(queue_name TEXT, qty INTEGER DEFAULT 1)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- result pgmq.message_record;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- DELETE from pgmq.%I- WHERE msg_id IN (select msg_id from cte)- RETURNING *;- $QUERY$,- qtable, qtable- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- Drop old set_vt overloads (they'll be recreated below)-DROP FUNCTION IF EXISTS pgmq.set_vt(TEXT, BIGINT, INTEGER);-DROP FUNCTION IF EXISTS pgmq.set_vt(TEXT, BIGINT, TIMESTAMP WITH TIME ZONE);-DROP FUNCTION IF EXISTS pgmq.set_vt(TEXT, BIGINT[], INTEGER);-DROP FUNCTION IF EXISTS pgmq.set_vt(TEXT, BIGINT[], TIMESTAMP WITH TIME ZONE);---- Sets timestamp vt of a message, returns it (base implementation)-CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt TIMESTAMP WITH TIME ZONE)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- result pgmq.message_record;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = $2- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_id;-END;-$$ LANGUAGE plpgsql;---- Sets integer vt of a message, returns it-CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt INTEGER)-RETURNS SETOF pgmq.message_record AS $$- SELECT * FROM pgmq.set_vt(queue_name, msg_id, clock_timestamp() + make_interval(secs => vt));-$$ LANGUAGE sql;---- Sets timestamp vt of multiple messages, returns them (base implementation)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = ANY($2)- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_ids;-END;-$$ LANGUAGE plpgsql;---- Sets integer vt of multiple messages, returns them-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt INTEGER-)-RETURNS SETOF pgmq.message_record AS $$- SELECT * FROM pgmq.set_vt(queue_name, msg_ids, clock_timestamp() + make_interval(secs => vt));-$$ LANGUAGE sql;
− database/v1.10.0/01_schema.sql
@@ -1,11 +0,0 @@---------------------------------------------------------------- Schema creation and grants (without extension)--------------------------------------------------------------CREATE SCHEMA IF NOT EXISTS pgmq;---- Grant permission to pg_monitor to all tables and sequences-GRANT USAGE ON SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL TABLES IN SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL SEQUENCES IN SCHEMA pgmq TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON TABLES TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON SEQUENCES TO pg_monitor;
− database/v1.10.0/02_tables.sql
@@ -1,25 +0,0 @@---------------------------------------------------------------- Tables: meta and notify_insert_throttle----------------------------------------------------------------- Table where queues and metadata about them is stored-CREATE TABLE IF NOT EXISTS pgmq.meta (- queue_name VARCHAR UNIQUE NOT NULL,- is_partitioned BOOLEAN NOT NULL,- is_unlogged BOOLEAN NOT NULL,- created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL-);---- Table to track notification throttling for queues-CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.notify_insert_throttle (- queue_name VARCHAR UNIQUE NOT NULL -- Queue name (without 'q_' prefix)- CONSTRAINT notify_insert_throttle_meta_queue_name_fk- REFERENCES pgmq.meta (queue_name)- ON DELETE CASCADE,- throttle_interval_ms INTEGER NOT NULL DEFAULT 0, -- Min milliseconds between notifications (0 = no throttling)- last_notified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT to_timestamp(0) -- Timestamp of last sent notification-);--CREATE INDEX IF NOT EXISTS idx_notify_throttle_active- ON pgmq.notify_insert_throttle (queue_name, last_notified_at)- WHERE throttle_interval_ms > 0;
− database/v1.10.0/03_types.sql
@@ -1,34 +0,0 @@---------------------------------------------------------------- Custom types: message_record, queue_record, metrics_result----------------------------------------------------------------- This type has the shape of a message in a queue, and is often returned by--- pgmq functions that return messages--- Note: last_read_at field added in pgmq 1.10.0-CREATE TYPE pgmq.message_record AS (- msg_id BIGINT,- read_ct INTEGER,- enqueued_at TIMESTAMP WITH TIME ZONE,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE,- message JSONB,- headers JSONB-);--CREATE TYPE pgmq.queue_record AS (- queue_name VARCHAR,- is_partitioned BOOLEAN,- is_unlogged BOOLEAN,- created_at TIMESTAMP WITH TIME ZONE-);---- returned by pgmq.metrics() and pgmq.metrics_all-CREATE TYPE pgmq.metrics_result AS (- queue_name text,- queue_length bigint,- newest_msg_age_sec int,- oldest_msg_age_sec int,- total_messages bigint,- scrape_time timestamp with time zone,- queue_visible_length bigint-);
− database/v1.10.0/04_core_functions.sql
@@ -1,69 +0,0 @@---------------------------------------------------------------- Core utility functions----------------------------------------------------------------- prevents race conditions during queue creation by acquiring a transaction-level advisory lock--- uses a transaction advisory lock maintain the lock until transaction commit--- a race condition would still exist if lock was released before commit-CREATE FUNCTION pgmq.acquire_queue_lock(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pg_advisory_xact_lock(hashtext('pgmq.queue_' || queue_name));-END;-$$ LANGUAGE plpgsql;---- a helper to format table names and check for invalid characters-CREATE FUNCTION pgmq.format_table_name(queue_name text, prefix text)-RETURNS TEXT AS $$-BEGIN- IF queue_name ~ '\$|;|--|'''- THEN- RAISE EXCEPTION 'queue name contains invalid characters: $, ;, --, or \''';- END IF;- RETURN lower(prefix || '_' || queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.validate_queue_name(queue_name TEXT)-RETURNS void AS $$-BEGIN- IF length(queue_name) > 47 THEN- -- complete table identifier must be <= 63- -- https://www.postgresql.org/docs/17/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS- -- e.g. template_pgmq_q_my_queue is an identifier for my_queue when partitioned- -- template_pgmq_q_ (16) + <a max length queue name> (47) = 63- RAISE EXCEPTION 'queue name is too long, maximum length is 47 characters';- END IF;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._belongs_to_pgmq(table_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BOOLEAN;-BEGIN- SELECT EXISTS (- SELECT 1- FROM pg_depend- WHERE refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pgmq')- AND objid = (- SELECT oid- FROM pg_class- WHERE relname = table_name- )- ) INTO result;- RETURN result;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._extension_exists(extension_name TEXT)- RETURNS BOOLEAN- LANGUAGE SQL-AS $$-SELECT EXISTS (- SELECT 1- FROM pg_extension- WHERE extname = extension_name-)-$$;
− database/v1.10.0/05_queue_management.sql
@@ -1,265 +0,0 @@---------------------------------------------------------------- Queue management functions---------------------------------------------------------------CREATE FUNCTION pgmq.create_non_partitioned(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create_unlogged(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, true)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq.create_non_partitioned(queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;-BEGIN- RAISE WARNING 'drop_queue(queue_name, partitioned) is deprecated and will be removed in PGMQ v2.0. Use drop_queue(queue_name) instead';-- PERFORM pgmq.drop_queue(queue_name);-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;- partitioned BOOLEAN;-BEGIN- PERFORM pgmq.acquire_queue_lock(queue_name);- EXECUTE FORMAT(- $QUERY$- SELECT is_partitioned FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- ) INTO partitioned;-- -- check if the queue exists- IF NOT EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = qtable and table_schema = 'pgmq'- ) THEN- RAISE NOTICE 'pgmq queue `%` does not exist', queue_name;- RETURN FALSE;- END IF;-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable- );-- IF EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = 'meta' and table_schema = 'pgmq'- ) THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- );- END IF;-- IF partitioned THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM %I.part_config where parent_table in (%L, %L)- $QUERY$,- pgmq._get_pg_partman_schema(), fq_qtable, fq_atable- );- END IF;-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;---- list queues-CREATE FUNCTION pgmq."list_queues"()-RETURNS SETOF pgmq.queue_record AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.meta;-END-$$ LANGUAGE plpgsql;---- purge queue, deleting all entries in it.-CREATE OR REPLACE FUNCTION pgmq."purge_queue"(queue_name TEXT)-RETURNS BIGINT AS $$-DECLARE- deleted_count INTEGER;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- -- Get the row count before truncating- EXECUTE format('SELECT count(*) FROM pgmq.%I', qtable) INTO deleted_count;-- -- Use TRUNCATE for better performance on large tables- EXECUTE format('TRUNCATE TABLE pgmq.%I', qtable);-- -- Return the number of purged rows- RETURN deleted_count;-END-$$ LANGUAGE plpgsql;---- unassign archive, so it can be kept when a queue is deleted-CREATE FUNCTION pgmq."detach_archive"(queue_name TEXT)-RETURNS VOID AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- RAISE WARNING 'detach_archive(queue_name) is deprecated and is a no-op. It will be removed in PGMQ v2.0. Archive tables are no longer member objects.';-END-$$ LANGUAGE plpgsql;
− database/v1.10.0/06_message_ops.sql
@@ -1,676 +0,0 @@---------------------------------------------------------------- Message operations: send, read, delete, archive, pop, set_vt----------------------------------------------------------------- send: 2 args, no delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with headers-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: 4 args with headers and delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: actual implementation-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- VALUES ($1, $2, $3)- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg, headers;-END;-$$ LANGUAGE plpgsql;---- send_batch: no delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers and delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: actual implementation-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- SELECT $1, unnest($2), unnest(coalesce($3, array_fill(NULL::jsonb, array[cardinality($2)])))- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msgs, headers;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them----- Note: last_read_at is set on read (pgmq 1.10.0+)-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- reads a number of messages from a queue, setting a visibility timeout on them----- 5-arg version without conditional filter-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 6th parameter (matches pgmq-hasql expected order)----- This is the overload used by pgmq-hasql: read_with_poll($1,$2,$3,$4,$5,$6)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- -- If conditional is NULL or empty, don't filter- IF conditional IS NULL OR conditional = '{}'::jsonb THEN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- ELSE- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- END IF;-- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 4th parameter (legacy order)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- delete--- deletes a message id from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id- $QUERY$,- qtable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- delete--- deletes an array of message ids from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- archive--- removes a message from the queue, and sends it to the archive--- Note: last_read_at is preserved during archival (pgmq 1.10.0+)-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- archive--- removes an array of message ids from the queue, and sends it to the archive-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- pop messages from queue (atomic read + delete)--- qty parameter added in pgmq 1.7.0--- Note: Returns message with last_read_at field (pgmq 1.10.0+)-CREATE FUNCTION pgmq.pop(- queue_name TEXT,- qty INTEGER DEFAULT 1-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- DELETE from pgmq.%I- WHERE msg_id IN (select msg_id from cte)- RETURNING *;- $QUERY$,- qtable, qtable- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- Sets vt of a single message, returns it--- Note: Returns message with last_read_at field (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_id BIGINT,- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = %L- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset), msg_id- );- RETURN QUERY EXECUTE sql;-END;-$$ LANGUAGE plpgsql;---- Batch set_vt for multiple messages (pgmq 1.8.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = ANY($1)- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset)- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- Sets timestamp vt of a message, returns it (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt TIMESTAMP WITH TIME ZONE)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = $2- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_id;-END;-$$ LANGUAGE plpgsql;---- Sets timestamp vt of multiple messages, returns them (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = ANY($2)- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_ids;-END;-$$ LANGUAGE plpgsql;
− database/v1.10.0/07_metrics.sql
@@ -1,83 +0,0 @@---------------------------------------------------------------- Metrics functions---------------------------------------------------------------CREATE FUNCTION pgmq.metrics(queue_name TEXT)-RETURNS pgmq.metrics_result AS $$-DECLARE- result_row pgmq.metrics_result;- query TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- query := FORMAT(- $QUERY$- WITH q_summary AS (- SELECT- count(*) as queue_length,- EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int as newest_msg_age_sec,- EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int as oldest_msg_age_sec,- NOW() as scrape_time,- count(*) FILTER (WHERE vt <= NOW()) AS queue_visible_length- FROM pgmq.%I- ),- all_metrics AS (- SELECT CASE- WHEN is_partitioned THEN- %L || '_part_' || pgmq._get_partition_col(%L)- ELSE- %L- END as queue_name- FROM pgmq.meta- WHERE meta.queue_name = %L- )- SELECT- m.queue_name,- q_summary.queue_length,- q_summary.newest_msg_age_sec,- q_summary.oldest_msg_age_sec,- pgmq._get_pg_stat_get_xact_tuples_inserted(%L)::bigint,- q_summary.scrape_time,- q_summary.queue_visible_length- FROM q_summary- CROSS JOIN all_metrics m- $QUERY$,- qtable, queue_name, queue_name, queue_name, queue_name, 'pgmq.' || qtable- );- EXECUTE query INTO result_row;- RETURN result_row;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.metrics_all()-RETURNS SETOF pgmq.metrics_result AS $$-DECLARE- row_name RECORD;- result_row pgmq.metrics_result;-BEGIN- FOR row_name IN SELECT queue_name FROM pgmq.meta LOOP- result_row := pgmq.metrics(row_name.queue_name);- RETURN NEXT result_row;- END LOOP;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_pg_stat_get_xact_tuples_inserted(table_name TEXT)-RETURNS bigint AS $$-DECLARE- result bigint;-BEGIN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = table_name;-- IF result IS NULL THEN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = SUBSTRING(table_name FROM 6); -- Remove 'pgmq.' prefix (5 chars + 1 for the dot)- END IF;-- RETURN COALESCE(result, 0);-END;-$$ LANGUAGE plpgsql;
− database/v1.10.0/08_partitioning.sql
@@ -1,268 +0,0 @@---------------------------------------------------------------- Partitioning support functions---------------------------------------------------------------CREATE FUNCTION pgmq._get_pg_partman_schema()-RETURNS TEXT AS $$- SELECT extnamespace::regnamespace::text- FROM pg_extension- WHERE extname = 'pg_partman';-$$ LANGUAGE sql STABLE;--CREATE FUNCTION pgmq._get_partition_col(queue_name TEXT)-RETURNS TEXT AS $$-DECLARE- _partition_col TEXT;- _table_name TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- SELECT part_config.partition_interval INTO _partition_col- FROM pg_class- JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid- JOIN (SELECT parent_table, partition_interval FROM pgmq._get_partman_config()) AS part_config- ON part_config.parent_table = pg_namespace.nspname || '.' || pg_class.relname- WHERE pg_class.relname = _table_name- AND pg_namespace.nspname = 'pgmq';-- RETURN _partition_col;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_partman_config()-RETURNS TABLE (parent_table text, partition_interval text) AS $$-DECLARE- schema_name TEXT;-BEGIN- SELECT pgmq._get_pg_partman_schema() INTO schema_name;- IF schema_name IS NULL THEN- RETURN QUERY SELECT NULL::text, NULL::text WHERE FALSE;- RETURN;- END IF;- RETURN QUERY EXECUTE FORMAT(- $QUERY$- SELECT parent_table, partition_interval- FROM %I.part_config- $QUERY$,- schema_name- );-END;-$$ LANGUAGE plpgsql;---- partitioned, with partition interval--- Note: last_read_at column added (pgmq 1.10.0+)-CREATE FUNCTION pgmq.create_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000'-)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');- atable_seq TEXT := atable || '_msg_id_seq';-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id)- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), qtable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, qtable- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, true, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;---- convert_archive_partitioned--- Note: last_read_at column added (pgmq 1.10.0+)-CREATE FUNCTION pgmq.convert_archive_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000',- leading_partition INT DEFAULT 10-)-RETURNS void AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- EXECUTE FORMAT(- $QUERY$- ALTER TABLE IF EXISTS pgmq.%I RENAME TO %I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := %L- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval, leading_partition- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.%I (msg_id, read_ct, enqueued_at, last_read_at, archived_at, vt, message, headers)- SELECT msg_id, read_ct, enqueued_at, last_read_at, archived_at, vt, message, headers FROM pgmq.%I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable || '_old'- );-END;-$$ LANGUAGE plpgsql;
− database/v1.10.0/09_notifications.sql
@@ -1,103 +0,0 @@---------------------------------------------------------------- Notification functions---------------------------------------------------------------CREATE FUNCTION pgmq._should_notify(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- should_notify BOOLEAN;- throttle_ms INTEGER;- last_notified TIMESTAMP WITH TIME ZONE;-BEGIN- -- Try to get throttle settings- SELECT throttle_interval_ms, last_notified_at- INTO throttle_ms, last_notified- FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name- FOR UPDATE SKIP LOCKED;-- -- If no row found, assume notifications are enabled without throttling- IF throttle_ms IS NULL THEN- RETURN TRUE;- END IF;-- -- Check if enough time has passed since last notification- IF clock_timestamp() >= last_notified + make_interval(secs => throttle_ms::numeric / 1000) THEN- -- Update the last notification time- UPDATE pgmq.notify_insert_throttle- SET last_notified_at = clock_timestamp()- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name;- RETURN TRUE;- END IF;-- RETURN FALSE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._notify_on_insert()-RETURNS TRIGGER AS $$-BEGIN- IF pgmq._should_notify(TG_ARGV[0]) THEN- PERFORM pg_notify(TG_ARGV[0], NEW.msg_id::text);- END IF;- RETURN NEW;-END;-$$ LANGUAGE plpgsql;---- Add trigger to queue table to notify on insert-CREATE FUNCTION pgmq.enable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- channel TEXT := queue_name;- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Create trigger on the queue table- EXECUTE FORMAT(- $QUERY$- CREATE TRIGGER %I- AFTER INSERT ON pgmq.%I- FOR EACH ROW- EXECUTE FUNCTION pgmq._notify_on_insert(%L)- $QUERY$,- trigger_name, qtable, channel- );-- -- Insert throttle tracking row if it doesn't exist- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (queue_name, 0, to_timestamp(0))- ON CONFLICT (queue_name) DO NOTHING;-END;-$$ LANGUAGE plpgsql;---- Remove trigger from queue table-CREATE FUNCTION pgmq.disable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Drop trigger if it exists- EXECUTE FORMAT(- $QUERY$- DROP TRIGGER IF EXISTS %I ON pgmq.%I- $QUERY$,- trigger_name, qtable- );-- -- Remove throttle tracking row- DELETE FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = disable_queue_notifications.queue_name;-END;-$$ LANGUAGE plpgsql;---- Set throttle interval for queue notifications-CREATE FUNCTION pgmq.set_notification_throttle(queue_name TEXT, throttle_interval_ms INTEGER)-RETURNS void AS $$-BEGIN- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (queue_name, throttle_interval_ms, to_timestamp(0))- ON CONFLICT (queue_name) DO UPDATE- SET throttle_interval_ms = EXCLUDED.throttle_interval_ms;-END;-$$ LANGUAGE plpgsql;
− database/v1.10.0/10_fifo.sql
@@ -1,571 +0,0 @@---------------------------------------------------------------- FIFO read functions (for pgmq 1.8.0+ and 1.9.0+)--- Note: last_read_at handling added in pgmq 1.10.0----------------------------------------------------------------- read_fifo: reads messages in strict FIFO order--- This function ensures messages are read in strict FIFO order by:--- 1. Ordering by msg_id ASC (not just using SKIP LOCKED)--- 2. Only returning messages that don't skip any earlier pending messages-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_fifo with conditional filter-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll: reads messages in strict FIFO order with polling-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll with conditional filter-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- FIFO index functions (pgmq 1.8.0+)----------------------------------------------------------------- _create_fifo_index_if_not_exists--- internal function to create GIN index on headers for better FIFO performance-CREATE OR REPLACE FUNCTION pgmq._create_fifo_index_if_not_exists(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- index_name TEXT := qtable || '_fifo_idx';-BEGIN- -- Create GIN index on headers for efficient FIFO key lookups- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I USING GIN (headers);- $QUERY$,- index_name, qtable- );-END;-$$ LANGUAGE plpgsql;---- create_fifo_index--- creates a GIN index on the headers column to improve FIFO read performance-CREATE FUNCTION pgmq.create_fifo_index(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq._create_fifo_index_if_not_exists(queue_name);-END;-$$ LANGUAGE plpgsql;---- create_fifo_indexes_all--- creates FIFO indexes on all existing queues-CREATE FUNCTION pgmq.create_fifo_indexes_all()-RETURNS void AS $$-DECLARE- queue_record RECORD;-BEGIN- FOR queue_record IN SELECT queue_name FROM pgmq.meta LOOP- PERFORM pgmq.create_fifo_index(queue_record.queue_name);- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- read_grouped functions (pgmq 1.8.0+)--- AWS SQS FIFO-style batch retrieval behavior----------------------------------------------------------------- read_grouped--- reads messages with AWS SQS FIFO-style batch retrieval behavior--- attempts to return as many messages as possible from the same message group-CREATE FUNCTION pgmq.read_grouped(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Find the minimum msg_id for each FIFO group that's ready to be processed- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,- MIN(msg_id) as min_msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- locked_groups AS (- -- Lock the first available message in each FIFO group- SELECT- m.msg_id,- fg.fifo_key- FROM pgmq.%I m- INNER JOIN fifo_groups fg ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key- AND m.msg_id = fg.min_msg_id- WHERE m.vt <= clock_timestamp()- ORDER BY m.msg_id ASC- FOR UPDATE SKIP LOCKED- ),- group_priorities AS (- -- Assign priority to groups based on their oldest message- SELECT- fifo_key,- msg_id as min_msg_id,- ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority- FROM locked_groups- ),- available_messages AS (- -- Get messages prioritizing filling batch from earliest group first- SELECT- m.msg_id,- gp.group_priority,- ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY m.msg_id) as msg_rank_in_group- FROM pgmq.%I m- INNER JOIN group_priorities gp ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= gp.min_msg_id -- Only messages from min_msg_id onwards in each group- AND NOT EXISTS (- -- Ensure no earlier message in this group is currently being processed- SELECT 1- FROM pgmq.%I m2- WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') =- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group')- AND m2.vt > clock_timestamp()- AND m2.msg_id < m.msg_id- )- ),- batch_selection AS (- -- Select messages to fill batch, prioritizing earliest group- SELECT- msg_id,- ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank- FROM available_messages- ),- selected_messages AS (- -- Limit to requested quantity- SELECT msg_id- FROM batch_selection- WHERE overall_rank <= $1- ORDER BY msg_id- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_with_poll--- reads messages with AWS SQS FIFO-style batch retrieval behavior, with polling support-CREATE FUNCTION pgmq.read_grouped_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- Round-robin FIFO functions (pgmq 1.9.0+)----------------------------------------------------------------- read_grouped_rr (round-robin)--- reads messages while preserving FIFO within groups and interleaving across groups (layered round-robin)-CREATE FUNCTION pgmq.read_grouped_rr(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Determine the absolute head (oldest) message id per FIFO group, regardless of visibility- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') AS fifo_key,- MIN(msg_id) AS head_msg_id- FROM pgmq.%1$I- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- eligible_groups AS (- -- Only groups whose head message is currently visible- -- Acquire a transaction-level advisory lock per group to prevent concurrent selection- SELECT- g.fifo_key,- g.head_msg_id,- ROW_NUMBER() OVER (ORDER BY g.head_msg_id) AS group_priority- FROM fifo_groups g- JOIN pgmq.%2$I h ON h.msg_id = g.head_msg_id- WHERE h.vt <= clock_timestamp()- AND pg_try_advisory_xact_lock(pg_catalog.hashtextextended(g.fifo_key, 0))- ),- available_messages AS (- -- All currently visible messages starting at the head for each eligible group- SELECT- m.msg_id,- eg.group_priority,- ROW_NUMBER() OVER (- PARTITION BY eg.fifo_key- ORDER BY m.msg_id- ) AS msg_rank_in_group- FROM pgmq.%3$I m- JOIN eligible_groups eg- ON COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = eg.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= eg.head_msg_id- ),- ordered_messages AS (- -- Layered round-robin: take rank 1 of all groups by group_priority, then rank 2, etc.- -- Assign selection order before locking- SELECT msg_id, ROW_NUMBER() OVER (ORDER BY msg_rank_in_group, group_priority) as selection_order- FROM available_messages- ),- selected_messages AS (- -- Lock the messages in the correct order, preserving selection_order- SELECT om.msg_id, om.selection_order- FROM ordered_messages om- JOIN pgmq.%4$I m ON m.msg_id = om.msg_id- WHERE om.selection_order <= $1- ORDER BY om.selection_order- FOR UPDATE OF m SKIP LOCKED- ),- updated_messages AS (- UPDATE pgmq.%5$I m- SET- vt = clock_timestamp() + %6$L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- AND m.vt <= clock_timestamp() -- final guard to avoid duplicate reads under races- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers, sm.selection_order- )- SELECT msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers- FROM updated_messages- ORDER BY selection_order;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_rr_with_poll--- reads messages using round-robin layering across groups, with polling support-CREATE FUNCTION pgmq.read_grouped_rr_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped_rr(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;
− database/v1.11.0/01_schema.sql
@@ -1,11 +0,0 @@---------------------------------------------------------------- Schema creation and grants (without extension)--------------------------------------------------------------CREATE SCHEMA IF NOT EXISTS pgmq;---- Grant permission to pg_monitor to all tables and sequences-GRANT USAGE ON SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL TABLES IN SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL SEQUENCES IN SCHEMA pgmq TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON TABLES TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON SEQUENCES TO pg_monitor;
− database/v1.11.0/02_tables.sql
@@ -1,49 +0,0 @@---------------------------------------------------------------- Tables: meta, notify_insert_throttle, topic_bindings----------------------------------------------------------------- Table where queues and metadata about them is stored-CREATE TABLE IF NOT EXISTS pgmq.meta (- queue_name VARCHAR UNIQUE NOT NULL,- is_partitioned BOOLEAN NOT NULL,- is_unlogged BOOLEAN NOT NULL,- created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL-);---- Table to track notification throttling for queues-CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.notify_insert_throttle (- queue_name VARCHAR UNIQUE NOT NULL -- Queue name (without 'q_' prefix)- CONSTRAINT notify_insert_throttle_meta_queue_name_fk- REFERENCES pgmq.meta (queue_name)- ON DELETE CASCADE,- throttle_interval_ms INTEGER NOT NULL DEFAULT 0, -- Min milliseconds between notifications (0 = no throttling)- last_notified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT to_timestamp(0) -- Timestamp of last sent notification-);--CREATE INDEX IF NOT EXISTS idx_notify_throttle_active- ON pgmq.notify_insert_throttle (queue_name, last_notified_at)- WHERE throttle_interval_ms > 0;---- Topic bindings for AMQP-like routing (pgmq 1.11.0+)-CREATE TABLE IF NOT EXISTS pgmq.topic_bindings-(- pattern text NOT NULL,- queue_name text NOT NULL- CONSTRAINT topic_bindings_meta_queue_name_fk- REFERENCES pgmq.meta (queue_name)- ON DELETE CASCADE,- bound_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- compiled_regex text GENERATED ALWAYS AS (- '^' ||- replace(- replace(- regexp_replace(pattern, '([.+?{}()|\[\]\\^$])', '\\\1', 'g'),- '*', '[^.]+'- ),- '#', '.*'- ) || '$'- ) STORED,- CONSTRAINT topic_bindings_unique_pattern_queue UNIQUE (pattern, queue_name)-);--CREATE INDEX IF NOT EXISTS idx_topic_bindings_covering ON pgmq.topic_bindings (pattern) INCLUDE (queue_name, compiled_regex);
− database/v1.11.0/03_types.sql
@@ -1,34 +0,0 @@---------------------------------------------------------------- Custom types: message_record, queue_record, metrics_result----------------------------------------------------------------- This type has the shape of a message in a queue, and is often returned by--- pgmq functions that return messages--- Note: last_read_at field added in pgmq 1.10.0-CREATE TYPE pgmq.message_record AS (- msg_id BIGINT,- read_ct INTEGER,- enqueued_at TIMESTAMP WITH TIME ZONE,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE,- message JSONB,- headers JSONB-);--CREATE TYPE pgmq.queue_record AS (- queue_name VARCHAR,- is_partitioned BOOLEAN,- is_unlogged BOOLEAN,- created_at TIMESTAMP WITH TIME ZONE-);---- returned by pgmq.metrics() and pgmq.metrics_all-CREATE TYPE pgmq.metrics_result AS (- queue_name text,- queue_length bigint,- newest_msg_age_sec int,- oldest_msg_age_sec int,- total_messages bigint,- scrape_time timestamp with time zone,- queue_visible_length bigint-);
− database/v1.11.0/04_core_functions.sql
@@ -1,69 +0,0 @@---------------------------------------------------------------- Core utility functions----------------------------------------------------------------- prevents race conditions during queue creation by acquiring a transaction-level advisory lock--- uses a transaction advisory lock maintain the lock until transaction commit--- a race condition would still exist if lock was released before commit-CREATE FUNCTION pgmq.acquire_queue_lock(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pg_advisory_xact_lock(hashtext('pgmq.queue_' || queue_name));-END;-$$ LANGUAGE plpgsql;---- a helper to format table names and check for invalid characters-CREATE FUNCTION pgmq.format_table_name(queue_name text, prefix text)-RETURNS TEXT AS $$-BEGIN- IF queue_name ~ '\$|;|--|'''- THEN- RAISE EXCEPTION 'queue name contains invalid characters: $, ;, --, or \''';- END IF;- RETURN lower(prefix || '_' || queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.validate_queue_name(queue_name TEXT)-RETURNS void AS $$-BEGIN- IF length(queue_name) > 47 THEN- -- complete table identifier must be <= 63- -- https://www.postgresql.org/docs/17/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS- -- e.g. template_pgmq_q_my_queue is an identifier for my_queue when partitioned- -- template_pgmq_q_ (16) + <a max length queue name> (47) = 63- RAISE EXCEPTION 'queue name is too long, maximum length is 47 characters';- END IF;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._belongs_to_pgmq(table_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BOOLEAN;-BEGIN- SELECT EXISTS (- SELECT 1- FROM pg_depend- WHERE refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pgmq')- AND objid = (- SELECT oid- FROM pg_class- WHERE relname = table_name- )- ) INTO result;- RETURN result;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._extension_exists(extension_name TEXT)- RETURNS BOOLEAN- LANGUAGE SQL-AS $$-SELECT EXISTS (- SELECT 1- FROM pg_extension- WHERE extname = extension_name-)-$$;
− database/v1.11.0/05_queue_management.sql
@@ -1,265 +0,0 @@---------------------------------------------------------------- Queue management functions---------------------------------------------------------------CREATE FUNCTION pgmq.create_non_partitioned(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create_unlogged(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, true)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq.create_non_partitioned(queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;-BEGIN- RAISE WARNING 'drop_queue(queue_name, partitioned) is deprecated and will be removed in PGMQ v2.0. Use drop_queue(queue_name) instead';-- PERFORM pgmq.drop_queue(queue_name);-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;- partitioned BOOLEAN;-BEGIN- PERFORM pgmq.acquire_queue_lock(queue_name);- EXECUTE FORMAT(- $QUERY$- SELECT is_partitioned FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- ) INTO partitioned;-- -- check if the queue exists- IF NOT EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = qtable and table_schema = 'pgmq'- ) THEN- RAISE NOTICE 'pgmq queue `%` does not exist', queue_name;- RETURN FALSE;- END IF;-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable- );-- IF EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = 'meta' and table_schema = 'pgmq'- ) THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- );- END IF;-- IF partitioned THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM %I.part_config where parent_table in (%L, %L)- $QUERY$,- pgmq._get_pg_partman_schema(), fq_qtable, fq_atable- );- END IF;-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;---- list queues-CREATE FUNCTION pgmq."list_queues"()-RETURNS SETOF pgmq.queue_record AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.meta;-END-$$ LANGUAGE plpgsql;---- purge queue, deleting all entries in it.-CREATE OR REPLACE FUNCTION pgmq."purge_queue"(queue_name TEXT)-RETURNS BIGINT AS $$-DECLARE- deleted_count INTEGER;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- -- Get the row count before truncating- EXECUTE format('SELECT count(*) FROM pgmq.%I', qtable) INTO deleted_count;-- -- Use TRUNCATE for better performance on large tables- EXECUTE format('TRUNCATE TABLE pgmq.%I', qtable);-- -- Return the number of purged rows- RETURN deleted_count;-END-$$ LANGUAGE plpgsql;---- unassign archive, so it can be kept when a queue is deleted-CREATE FUNCTION pgmq."detach_archive"(queue_name TEXT)-RETURNS VOID AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- RAISE WARNING 'detach_archive(queue_name) is deprecated and is a no-op. It will be removed in PGMQ v2.0. Archive tables are no longer member objects.';-END-$$ LANGUAGE plpgsql;
− database/v1.11.0/06_message_ops.sql
@@ -1,676 +0,0 @@---------------------------------------------------------------- Message operations: send, read, delete, archive, pop, set_vt----------------------------------------------------------------- send: 2 args, no delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with headers-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: 4 args with headers and delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: actual implementation-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- VALUES ($1, $2, $3)- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg, headers;-END;-$$ LANGUAGE plpgsql;---- send_batch: no delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers and delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: actual implementation-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- SELECT $1, unnest($2), unnest(coalesce($3, array_fill(NULL::jsonb, array[cardinality($2)])))- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msgs, headers;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them----- Note: last_read_at is set on read (pgmq 1.10.0+)-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- reads a number of messages from a queue, setting a visibility timeout on them----- 5-arg version without conditional filter-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 6th parameter (matches pgmq-hasql expected order)----- This is the overload used by pgmq-hasql: read_with_poll($1,$2,$3,$4,$5,$6)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- -- If conditional is NULL or empty, don't filter- IF conditional IS NULL OR conditional = '{}'::jsonb THEN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- ELSE- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- END IF;-- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 4th parameter (legacy order)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- delete--- deletes a message id from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id- $QUERY$,- qtable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- delete--- deletes an array of message ids from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- archive--- removes a message from the queue, and sends it to the archive--- Note: last_read_at is preserved during archival (pgmq 1.10.0+)-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- archive--- removes an array of message ids from the queue, and sends it to the archive-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- pop messages from queue (atomic read + delete)--- qty parameter added in pgmq 1.7.0--- Note: Returns message with last_read_at field (pgmq 1.10.0+)-CREATE FUNCTION pgmq.pop(- queue_name TEXT,- qty INTEGER DEFAULT 1-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- DELETE from pgmq.%I- WHERE msg_id IN (select msg_id from cte)- RETURNING *;- $QUERY$,- qtable, qtable- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- Sets vt of a single message, returns it--- Note: Returns message with last_read_at field (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_id BIGINT,- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = %L- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset), msg_id- );- RETURN QUERY EXECUTE sql;-END;-$$ LANGUAGE plpgsql;---- Batch set_vt for multiple messages (pgmq 1.8.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = ANY($1)- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset)- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- Sets timestamp vt of a message, returns it (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt TIMESTAMP WITH TIME ZONE)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = $2- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_id;-END;-$$ LANGUAGE plpgsql;---- Sets timestamp vt of multiple messages, returns them (pgmq 1.10.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = $1- WHERE msg_id = ANY($2)- RETURNING *;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg_ids;-END;-$$ LANGUAGE plpgsql;
− database/v1.11.0/07_metrics.sql
@@ -1,83 +0,0 @@---------------------------------------------------------------- Metrics functions---------------------------------------------------------------CREATE FUNCTION pgmq.metrics(queue_name TEXT)-RETURNS pgmq.metrics_result AS $$-DECLARE- result_row pgmq.metrics_result;- query TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- query := FORMAT(- $QUERY$- WITH q_summary AS (- SELECT- count(*) as queue_length,- EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int as newest_msg_age_sec,- EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int as oldest_msg_age_sec,- NOW() as scrape_time,- count(*) FILTER (WHERE vt <= NOW()) AS queue_visible_length- FROM pgmq.%I- ),- all_metrics AS (- SELECT CASE- WHEN is_partitioned THEN- %L || '_part_' || pgmq._get_partition_col(%L)- ELSE- %L- END as queue_name- FROM pgmq.meta- WHERE meta.queue_name = %L- )- SELECT- m.queue_name,- q_summary.queue_length,- q_summary.newest_msg_age_sec,- q_summary.oldest_msg_age_sec,- pgmq._get_pg_stat_get_xact_tuples_inserted(%L)::bigint,- q_summary.scrape_time,- q_summary.queue_visible_length- FROM q_summary- CROSS JOIN all_metrics m- $QUERY$,- qtable, queue_name, queue_name, queue_name, queue_name, 'pgmq.' || qtable- );- EXECUTE query INTO result_row;- RETURN result_row;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.metrics_all()-RETURNS SETOF pgmq.metrics_result AS $$-DECLARE- row_name RECORD;- result_row pgmq.metrics_result;-BEGIN- FOR row_name IN SELECT queue_name FROM pgmq.meta LOOP- result_row := pgmq.metrics(row_name.queue_name);- RETURN NEXT result_row;- END LOOP;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_pg_stat_get_xact_tuples_inserted(table_name TEXT)-RETURNS bigint AS $$-DECLARE- result bigint;-BEGIN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = table_name;-- IF result IS NULL THEN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = SUBSTRING(table_name FROM 6); -- Remove 'pgmq.' prefix (5 chars + 1 for the dot)- END IF;-- RETURN COALESCE(result, 0);-END;-$$ LANGUAGE plpgsql;
− database/v1.11.0/08_partitioning.sql
@@ -1,268 +0,0 @@---------------------------------------------------------------- Partitioning support functions---------------------------------------------------------------CREATE FUNCTION pgmq._get_pg_partman_schema()-RETURNS TEXT AS $$- SELECT extnamespace::regnamespace::text- FROM pg_extension- WHERE extname = 'pg_partman';-$$ LANGUAGE sql STABLE;--CREATE FUNCTION pgmq._get_partition_col(queue_name TEXT)-RETURNS TEXT AS $$-DECLARE- _partition_col TEXT;- _table_name TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- SELECT part_config.partition_interval INTO _partition_col- FROM pg_class- JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid- JOIN (SELECT parent_table, partition_interval FROM pgmq._get_partman_config()) AS part_config- ON part_config.parent_table = pg_namespace.nspname || '.' || pg_class.relname- WHERE pg_class.relname = _table_name- AND pg_namespace.nspname = 'pgmq';-- RETURN _partition_col;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_partman_config()-RETURNS TABLE (parent_table text, partition_interval text) AS $$-DECLARE- schema_name TEXT;-BEGIN- SELECT pgmq._get_pg_partman_schema() INTO schema_name;- IF schema_name IS NULL THEN- RETURN QUERY SELECT NULL::text, NULL::text WHERE FALSE;- RETURN;- END IF;- RETURN QUERY EXECUTE FORMAT(- $QUERY$- SELECT parent_table, partition_interval- FROM %I.part_config- $QUERY$,- schema_name- );-END;-$$ LANGUAGE plpgsql;---- partitioned, with partition interval--- Note: last_read_at column added (pgmq 1.10.0+)-CREATE FUNCTION pgmq.create_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000'-)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');- atable_seq TEXT := atable || '_msg_id_seq';-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id)- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), qtable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, qtable- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, true, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;---- convert_archive_partitioned--- Note: last_read_at column added (pgmq 1.10.0+)-CREATE FUNCTION pgmq.convert_archive_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000',- leading_partition INT DEFAULT 10-)-RETURNS void AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- EXECUTE FORMAT(- $QUERY$- ALTER TABLE IF EXISTS pgmq.%I RENAME TO %I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- last_read_at TIMESTAMP WITH TIME ZONE,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := %L- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval, leading_partition- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.%I (msg_id, read_ct, enqueued_at, last_read_at, archived_at, vt, message, headers)- SELECT msg_id, read_ct, enqueued_at, last_read_at, archived_at, vt, message, headers FROM pgmq.%I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable || '_old'- );-END;-$$ LANGUAGE plpgsql;
− database/v1.11.0/09_notifications.sql
@@ -1,167 +0,0 @@---------------------------------------------------------------- Notification functions---------------------------------------------------------------CREATE FUNCTION pgmq._should_notify(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- should_notify BOOLEAN;- throttle_ms INTEGER;- last_notified TIMESTAMP WITH TIME ZONE;-BEGIN- -- Try to get throttle settings- SELECT throttle_interval_ms, last_notified_at- INTO throttle_ms, last_notified- FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name- FOR UPDATE SKIP LOCKED;-- -- If no row found, assume notifications are enabled without throttling- IF throttle_ms IS NULL THEN- RETURN TRUE;- END IF;-- -- Check if enough time has passed since last notification- IF clock_timestamp() >= last_notified + make_interval(secs => throttle_ms::numeric / 1000) THEN- -- Update the last notification time- UPDATE pgmq.notify_insert_throttle- SET last_notified_at = clock_timestamp()- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name;- RETURN TRUE;- END IF;-- RETURN FALSE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._notify_on_insert()-RETURNS TRIGGER AS $$-BEGIN- IF pgmq._should_notify(TG_ARGV[0]) THEN- PERFORM pg_notify(TG_ARGV[0], NEW.msg_id::text);- END IF;- RETURN NEW;-END;-$$ LANGUAGE plpgsql;---- Add trigger to queue table to notify on insert-CREATE FUNCTION pgmq.enable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-#variable_conflict use_column-DECLARE- qtable TEXT := pgmq.format_table_name(enable_queue_notifications.queue_name, 'q');- channel TEXT := enable_queue_notifications.queue_name;- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Create trigger on the queue table- EXECUTE FORMAT(- $QUERY$- CREATE TRIGGER %I- AFTER INSERT ON pgmq.%I- FOR EACH ROW- EXECUTE FUNCTION pgmq._notify_on_insert(%L)- $QUERY$,- trigger_name, qtable, channel- );-- -- Insert throttle tracking row if it doesn't exist- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (enable_queue_notifications.queue_name, 0, to_timestamp(0))- ON CONFLICT (queue_name) DO NOTHING;-END;-$$ LANGUAGE plpgsql;---- Remove trigger from queue table-CREATE FUNCTION pgmq.disable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Drop trigger if it exists- EXECUTE FORMAT(- $QUERY$- DROP TRIGGER IF EXISTS %I ON pgmq.%I- $QUERY$,- trigger_name, qtable- );-- -- Remove throttle tracking row- DELETE FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = disable_queue_notifications.queue_name;-END;-$$ LANGUAGE plpgsql;---- Set throttle interval for queue notifications-CREATE FUNCTION pgmq.set_notification_throttle(queue_name TEXT, throttle_interval_ms INTEGER)-RETURNS void AS $$-#variable_conflict use_column-BEGIN- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (set_notification_throttle.queue_name, set_notification_throttle.throttle_interval_ms, to_timestamp(0))- ON CONFLICT (queue_name) DO UPDATE- SET throttle_interval_ms = EXCLUDED.throttle_interval_ms;-END;-$$ LANGUAGE plpgsql;---- Enable insert notifications (upstream-compatible name)-CREATE FUNCTION pgmq.enable_notify_insert(queue_name TEXT, throttle_interval_ms INTEGER DEFAULT NULL)-RETURNS void AS $$-BEGIN- PERFORM pgmq.enable_queue_notifications(queue_name);- IF throttle_interval_ms IS NOT NULL THEN- PERFORM pgmq.set_notification_throttle(queue_name, throttle_interval_ms);- END IF;-END;-$$ LANGUAGE plpgsql;---- Disable insert notifications (upstream-compatible name)-CREATE FUNCTION pgmq.disable_notify_insert(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq.disable_queue_notifications(queue_name);-END;-$$ LANGUAGE plpgsql;---- List all notification insert throttle settings (pgmq 1.11.0+)-CREATE FUNCTION pgmq.list_notify_insert_throttles()- RETURNS TABLE- (- queue_name text,- throttle_interval_ms integer,- last_notified_at TIMESTAMP WITH TIME ZONE- )- LANGUAGE sql- STABLE-AS-$$- SELECT queue_name, throttle_interval_ms, last_notified_at- FROM pgmq.notify_insert_throttle- ORDER BY queue_name;-$$;---- Update the throttle interval for a queue's insert notifications (pgmq 1.11.0+)-CREATE FUNCTION pgmq.update_notify_insert(queue_name text, throttle_interval_ms integer)- RETURNS void- LANGUAGE plpgsql-AS-$$-BEGIN- IF throttle_interval_ms < 0 THEN- RAISE EXCEPTION 'throttle_interval_ms must be non-negative, got: %', throttle_interval_ms;- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = update_notify_insert.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.notify_insert_throttle WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not have notify_insert enabled. Enable it first using pgmq.enable_notify_insert()', queue_name;- END IF;-- UPDATE pgmq.notify_insert_throttle- SET throttle_interval_ms = update_notify_insert.throttle_interval_ms,- last_notified_at = to_timestamp(0)- WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name;-END;-$$;
− database/v1.11.0/10_fifo.sql
@@ -1,571 +0,0 @@---------------------------------------------------------------- FIFO read functions (for pgmq 1.8.0+ and 1.9.0+)--- Note: last_read_at handling added in pgmq 1.10.0----------------------------------------------------------------- read_fifo: reads messages in strict FIFO order--- This function ensures messages are read in strict FIFO order by:--- 1. Ordering by msg_id ASC (not just using SKIP LOCKED)--- 2. Only returning messages that don't skip any earlier pending messages-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_fifo with conditional filter-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll: reads messages in strict FIFO order with polling-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll with conditional filter-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- FIFO index functions (pgmq 1.8.0+)----------------------------------------------------------------- _create_fifo_index_if_not_exists--- internal function to create GIN index on headers for better FIFO performance-CREATE OR REPLACE FUNCTION pgmq._create_fifo_index_if_not_exists(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- index_name TEXT := qtable || '_fifo_idx';-BEGIN- -- Create GIN index on headers for efficient FIFO key lookups- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I USING GIN (headers);- $QUERY$,- index_name, qtable- );-END;-$$ LANGUAGE plpgsql;---- create_fifo_index--- creates a GIN index on the headers column to improve FIFO read performance-CREATE FUNCTION pgmq.create_fifo_index(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq._create_fifo_index_if_not_exists(queue_name);-END;-$$ LANGUAGE plpgsql;---- create_fifo_indexes_all--- creates FIFO indexes on all existing queues-CREATE FUNCTION pgmq.create_fifo_indexes_all()-RETURNS void AS $$-DECLARE- queue_record RECORD;-BEGIN- FOR queue_record IN SELECT queue_name FROM pgmq.meta LOOP- PERFORM pgmq.create_fifo_index(queue_record.queue_name);- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- read_grouped functions (pgmq 1.8.0+)--- AWS SQS FIFO-style batch retrieval behavior----------------------------------------------------------------- read_grouped--- reads messages with AWS SQS FIFO-style batch retrieval behavior--- attempts to return as many messages as possible from the same message group-CREATE FUNCTION pgmq.read_grouped(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Find the minimum msg_id for each FIFO group that's ready to be processed- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,- MIN(msg_id) as min_msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- locked_groups AS (- -- Lock the first available message in each FIFO group- SELECT- m.msg_id,- fg.fifo_key- FROM pgmq.%I m- INNER JOIN fifo_groups fg ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key- AND m.msg_id = fg.min_msg_id- WHERE m.vt <= clock_timestamp()- ORDER BY m.msg_id ASC- FOR UPDATE SKIP LOCKED- ),- group_priorities AS (- -- Assign priority to groups based on their oldest message- SELECT- fifo_key,- msg_id as min_msg_id,- ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority- FROM locked_groups- ),- available_messages AS (- -- Get messages prioritizing filling batch from earliest group first- SELECT- m.msg_id,- gp.group_priority,- ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY m.msg_id) as msg_rank_in_group- FROM pgmq.%I m- INNER JOIN group_priorities gp ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= gp.min_msg_id -- Only messages from min_msg_id onwards in each group- AND NOT EXISTS (- -- Ensure no earlier message in this group is currently being processed- SELECT 1- FROM pgmq.%I m2- WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') =- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group')- AND m2.vt > clock_timestamp()- AND m2.msg_id < m.msg_id- )- ),- batch_selection AS (- -- Select messages to fill batch, prioritizing earliest group- SELECT- msg_id,- ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank- FROM available_messages- ),- selected_messages AS (- -- Limit to requested quantity- SELECT msg_id- FROM batch_selection- WHERE overall_rank <= $1- ORDER BY msg_id- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_with_poll--- reads messages with AWS SQS FIFO-style batch retrieval behavior, with polling support-CREATE FUNCTION pgmq.read_grouped_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- Round-robin FIFO functions (pgmq 1.9.0+)----------------------------------------------------------------- read_grouped_rr (round-robin)--- reads messages while preserving FIFO within groups and interleaving across groups (layered round-robin)-CREATE FUNCTION pgmq.read_grouped_rr(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Determine the absolute head (oldest) message id per FIFO group, regardless of visibility- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') AS fifo_key,- MIN(msg_id) AS head_msg_id- FROM pgmq.%1$I- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- eligible_groups AS (- -- Only groups whose head message is currently visible- -- Acquire a transaction-level advisory lock per group to prevent concurrent selection- SELECT- g.fifo_key,- g.head_msg_id,- ROW_NUMBER() OVER (ORDER BY g.head_msg_id) AS group_priority- FROM fifo_groups g- JOIN pgmq.%2$I h ON h.msg_id = g.head_msg_id- WHERE h.vt <= clock_timestamp()- AND pg_try_advisory_xact_lock(pg_catalog.hashtextextended(g.fifo_key, 0))- ),- available_messages AS (- -- All currently visible messages starting at the head for each eligible group- SELECT- m.msg_id,- eg.group_priority,- ROW_NUMBER() OVER (- PARTITION BY eg.fifo_key- ORDER BY m.msg_id- ) AS msg_rank_in_group- FROM pgmq.%3$I m- JOIN eligible_groups eg- ON COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = eg.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= eg.head_msg_id- ),- ordered_messages AS (- -- Layered round-robin: take rank 1 of all groups by group_priority, then rank 2, etc.- -- Assign selection order before locking- SELECT msg_id, ROW_NUMBER() OVER (ORDER BY msg_rank_in_group, group_priority) as selection_order- FROM available_messages- ),- selected_messages AS (- -- Lock the messages in the correct order, preserving selection_order- SELECT om.msg_id, om.selection_order- FROM ordered_messages om- JOIN pgmq.%4$I m ON m.msg_id = om.msg_id- WHERE om.selection_order <= $1- ORDER BY om.selection_order- FOR UPDATE OF m SKIP LOCKED- ),- updated_messages AS (- UPDATE pgmq.%5$I m- SET- vt = clock_timestamp() + %6$L,- read_ct = read_ct + 1,- last_read_at = clock_timestamp()- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- AND m.vt <= clock_timestamp() -- final guard to avoid duplicate reads under races- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers, sm.selection_order- )- SELECT msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers- FROM updated_messages- ORDER BY selection_order;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_rr_with_poll--- reads messages using round-robin layering across groups, with polling support-CREATE FUNCTION pgmq.read_grouped_rr_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped_rr(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;
− database/v1.11.0/11_topics.sql
@@ -1,395 +0,0 @@---------------------------------------------------------------- Topic routing functions (pgmq 1.11.0+)---------------------------------------------------------------CREATE OR REPLACE FUNCTION pgmq.validate_routing_key(routing_key text)- RETURNS boolean- LANGUAGE plpgsql- IMMUTABLE-AS-$$-BEGIN- IF routing_key IS NULL OR routing_key = '' THEN- RAISE EXCEPTION 'routing_key cannot be NULL or empty';- END IF;-- IF length(routing_key) > 255 THEN- RAISE EXCEPTION 'routing_key length cannot exceed 255 characters, got % characters', length(routing_key);- END IF;-- IF routing_key !~ '^[a-zA-Z0-9._-]+$' THEN- RAISE EXCEPTION 'routing_key contains invalid characters. Only alphanumeric, dots, hyphens, and underscores are allowed. Got: %', routing_key;- END IF;-- IF routing_key ~ '^\.' THEN- RAISE EXCEPTION 'routing_key cannot start with a dot. Got: %', routing_key;- END IF;-- IF routing_key ~ '\.$' THEN- RAISE EXCEPTION 'routing_key cannot end with a dot. Got: %', routing_key;- END IF;-- IF routing_key ~ '\.\.' THEN- RAISE EXCEPTION 'routing_key cannot contain consecutive dots. Got: %', routing_key;- END IF;-- RETURN true;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.validate_topic_pattern(pattern text)- RETURNS boolean- LANGUAGE plpgsql- IMMUTABLE-AS-$$-BEGIN- IF pattern IS NULL OR pattern = '' THEN- RAISE EXCEPTION 'pattern cannot be NULL or empty';- END IF;-- IF length(pattern) > 255 THEN- RAISE EXCEPTION 'pattern length cannot exceed 255 characters, got % characters', length(pattern);- END IF;-- IF pattern !~ '^[a-zA-Z0-9._\-*#]+$' THEN- RAISE EXCEPTION 'pattern contains invalid characters. Only alphanumeric, dots, hyphens, underscores, *, and # are allowed. Got: %', pattern;- END IF;-- IF pattern ~ '^\.' THEN- RAISE EXCEPTION 'pattern cannot start with a dot. Got: %', pattern;- END IF;-- IF pattern ~ '\.$' THEN- RAISE EXCEPTION 'pattern cannot end with a dot. Got: %', pattern;- END IF;-- IF pattern ~ '\.\.' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive dots. Got: %', pattern;- END IF;-- IF pattern ~ '\*\*' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive stars (**). Use # for multi-segment matching. Got: %', pattern;- END IF;-- IF pattern ~ '##' THEN- RAISE EXCEPTION 'pattern cannot contain consecutive hashes (##). A single # already matches zero or more segments. Got: %', pattern;- END IF;-- IF pattern ~ '\*#' OR pattern ~ '#\*' THEN- RAISE EXCEPTION 'pattern cannot contain adjacent wildcards (*# or #*). Separate wildcards with dots. Got: %', pattern;- END IF;-- RETURN true;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.bind_topic(pattern text, queue_name text)- RETURNS void- LANGUAGE plpgsql-AS-$$-BEGIN- PERFORM pgmq.validate_topic_pattern(pattern);- IF queue_name IS NULL OR queue_name = '' THEN- RAISE EXCEPTION 'queue_name cannot be NULL or empty';- END IF;-- IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = bind_topic.queue_name) THEN- RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;- END IF;-- INSERT INTO pgmq.topic_bindings (pattern, queue_name)- VALUES (pattern, queue_name)- ON CONFLICT ON CONSTRAINT topic_bindings_unique_pattern_queue DO NOTHING;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.unbind_topic(pattern text, queue_name text)- RETURNS boolean- LANGUAGE plpgsql-AS-$$-DECLARE- rows_deleted integer;-BEGIN- IF pattern IS NULL OR pattern = '' THEN- RAISE EXCEPTION 'pattern cannot be NULL or empty';- END IF;-- IF queue_name IS NULL OR queue_name = '' THEN- RAISE EXCEPTION 'queue_name cannot be NULL or empty';- END IF;-- DELETE- FROM pgmq.topic_bindings- WHERE topic_bindings.pattern = unbind_topic.pattern- AND topic_bindings.queue_name = unbind_topic.queue_name;-- GET DIAGNOSTICS rows_deleted = ROW_COUNT;-- IF rows_deleted > 0 THEN- RETURN true;- ELSE- RETURN false;- END IF;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.test_routing(routing_key text)- RETURNS TABLE- (- pattern text,- queue_name text,- compiled_regex text- )- LANGUAGE plpgsql- STABLE-AS-$$-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);- RETURN QUERY- SELECT b.pattern,- b.queue_name,- b.compiled_regex- FROM pgmq.topic_bindings b- WHERE routing_key ~ b.compiled_regex- ORDER BY b.pattern;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, headers jsonb, delay integer)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-DECLARE- b RECORD;- matched_count integer := 0;-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);-- IF msg IS NULL THEN- RAISE EXCEPTION 'msg cannot be NULL';- END IF;-- IF delay < 0 THEN- RAISE EXCEPTION 'delay cannot be negative, got: %', delay;- END IF;-- FOR b IN- SELECT DISTINCT tb.queue_name- FROM pgmq.topic_bindings tb- WHERE routing_key ~ tb.compiled_regex- ORDER BY tb.queue_name- LOOP- PERFORM pgmq.send(b.queue_name, msg, headers, delay);- matched_count := matched_count + 1;- END LOOP;-- RETURN matched_count;-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-BEGIN- RETURN pgmq.send_topic(routing_key, msg, NULL, 0);-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, delay integer)- RETURNS integer- LANGUAGE plpgsql- VOLATILE-AS-$$-BEGIN- RETURN pgmq.send_topic(routing_key, msg, NULL, delay);-END;-$$;--CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings()- RETURNS TABLE- (- pattern text,- queue_name text,- bound_at TIMESTAMP WITH TIME ZONE,- compiled_regex text- )- LANGUAGE sql- STABLE-AS-$$- SELECT pattern, queue_name, bound_at, compiled_regex- FROM pgmq.topic_bindings- ORDER BY bound_at DESC, pattern, queue_name;-$$;--CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings(queue_name text)- RETURNS TABLE- (- pattern text,- queue_name text,- bound_at TIMESTAMP WITH TIME ZONE,- compiled_regex text- )- LANGUAGE sql- STABLE-AS-$$- SELECT pattern, tb.queue_name, bound_at, compiled_regex- FROM pgmq.topic_bindings tb- WHERE tb.queue_name = list_topic_bindings.queue_name- ORDER BY bound_at DESC, pattern;-$$;---- _validate_batch_params: Private function to validate batch parameters-CREATE OR REPLACE FUNCTION pgmq._validate_batch_params(- msgs JSONB[],- headers JSONB[]-) RETURNS void AS $$-BEGIN- IF msgs IS NULL OR array_length(msgs, 1) IS NULL THEN- RAISE EXCEPTION 'msgs cannot be NULL or empty';- END IF;-- IF headers IS NOT NULL AND COALESCE(array_length(headers, 1), 0) != COALESCE(array_length(msgs, 1), 0) THEN- RAISE EXCEPTION 'headers array length (%) must match msgs array length (%)',- COALESCE(array_length(headers, 1), 0), COALESCE(array_length(msgs, 1), 0);- END IF;-END;-$$ LANGUAGE plpgsql;---- _send_batch: Private function that performs the actual batch insert without validation-CREATE OR REPLACE FUNCTION pgmq._send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay TIMESTAMP WITH TIME ZONE-) RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- SELECT $2, unnest($1), unnest(coalesce($3, ARRAY[]::jsonb[]))- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING msgs, delay, headers;-END;-$$ LANGUAGE plpgsql;---- send_batch_topic: Base implementation with TIMESTAMP WITH TIME ZONE delay-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[],- delay TIMESTAMP WITH TIME ZONE-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE plpgsql- VOLATILE-AS-$$-DECLARE- b RECORD;-BEGIN- PERFORM pgmq.validate_routing_key(routing_key);-- PERFORM pgmq._validate_batch_params(msgs, headers);-- FOR b IN- SELECT DISTINCT tb.queue_name- FROM pgmq.topic_bindings tb- WHERE routing_key ~ tb.compiled_regex- ORDER BY tb.queue_name- LOOP- RETURN QUERY- SELECT b.queue_name, batch_result.msg_id- FROM pgmq._send_batch(b.queue_name, msgs, headers, delay) AS batch_result(msg_id);- END LOOP;-- RETURN;-END;-$$;---- send_batch_topic: 2 args (routing_key, msgs)-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[]-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp());-$$;---- send_batch_topic: 3 args with headers-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[]-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp());-$$;---- send_batch_topic: 3 args with integer delay-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- delay integer-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp() + make_interval(secs => delay));-$$;---- send_batch_topic: 3 args with timestamp delay-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- delay TIMESTAMP WITH TIME ZONE-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, delay);-$$;---- send_batch_topic: 4 args with integer delay-CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(- routing_key text,- msgs jsonb[],- headers jsonb[],- delay integer-)- RETURNS TABLE(queue_name text, msg_id bigint)- LANGUAGE sql- VOLATILE-AS-$$- SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp() + make_interval(secs => delay));-$$;
− database/v1.9.0/01_schema.sql
@@ -1,11 +0,0 @@---------------------------------------------------------------- Schema creation and grants (without extension)--------------------------------------------------------------CREATE SCHEMA IF NOT EXISTS pgmq;---- Grant permission to pg_monitor to all tables and sequences-GRANT USAGE ON SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL TABLES IN SCHEMA pgmq TO pg_monitor;-GRANT SELECT ON ALL SEQUENCES IN SCHEMA pgmq TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON TABLES TO pg_monitor;-ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON SEQUENCES TO pg_monitor;
− database/v1.9.0/02_tables.sql
@@ -1,25 +0,0 @@---------------------------------------------------------------- Tables: meta and notify_insert_throttle----------------------------------------------------------------- Table where queues and metadata about them is stored-CREATE TABLE IF NOT EXISTS pgmq.meta (- queue_name VARCHAR UNIQUE NOT NULL,- is_partitioned BOOLEAN NOT NULL,- is_unlogged BOOLEAN NOT NULL,- created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL-);---- Table to track notification throttling for queues-CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.notify_insert_throttle (- queue_name VARCHAR UNIQUE NOT NULL -- Queue name (without 'q_' prefix)- CONSTRAINT notify_insert_throttle_meta_queue_name_fk- REFERENCES pgmq.meta (queue_name)- ON DELETE CASCADE,- throttle_interval_ms INTEGER NOT NULL DEFAULT 0, -- Min milliseconds between notifications (0 = no throttling)- last_notified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT to_timestamp(0) -- Timestamp of last sent notification-);--CREATE INDEX IF NOT EXISTS idx_notify_throttle_active- ON pgmq.notify_insert_throttle (queue_name, last_notified_at)- WHERE throttle_interval_ms > 0;
− database/v1.9.0/03_types.sql
@@ -1,32 +0,0 @@---------------------------------------------------------------- Custom types: message_record, queue_record, metrics_result----------------------------------------------------------------- This type has the shape of a message in a queue, and is often returned by--- pgmq functions that return messages-CREATE TYPE pgmq.message_record AS (- msg_id BIGINT,- read_ct INTEGER,- enqueued_at TIMESTAMP WITH TIME ZONE,- vt TIMESTAMP WITH TIME ZONE,- message JSONB,- headers JSONB-);--CREATE TYPE pgmq.queue_record AS (- queue_name VARCHAR,- is_partitioned BOOLEAN,- is_unlogged BOOLEAN,- created_at TIMESTAMP WITH TIME ZONE-);---- returned by pgmq.metrics() and pgmq.metrics_all-CREATE TYPE pgmq.metrics_result AS (- queue_name text,- queue_length bigint,- newest_msg_age_sec int,- oldest_msg_age_sec int,- total_messages bigint,- scrape_time timestamp with time zone,- queue_visible_length bigint-);
− database/v1.9.0/04_core_functions.sql
@@ -1,69 +0,0 @@---------------------------------------------------------------- Core utility functions----------------------------------------------------------------- prevents race conditions during queue creation by acquiring a transaction-level advisory lock--- uses a transaction advisory lock maintain the lock until transaction commit--- a race condition would still exist if lock was released before commit-CREATE FUNCTION pgmq.acquire_queue_lock(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pg_advisory_xact_lock(hashtext('pgmq.queue_' || queue_name));-END;-$$ LANGUAGE plpgsql;---- a helper to format table names and check for invalid characters-CREATE FUNCTION pgmq.format_table_name(queue_name text, prefix text)-RETURNS TEXT AS $$-BEGIN- IF queue_name ~ '\$|;|--|'''- THEN- RAISE EXCEPTION 'queue name contains invalid characters: $, ;, --, or \''';- END IF;- RETURN lower(prefix || '_' || queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.validate_queue_name(queue_name TEXT)-RETURNS void AS $$-BEGIN- IF length(queue_name) > 47 THEN- -- complete table identifier must be <= 63- -- https://www.postgresql.org/docs/17/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS- -- e.g. template_pgmq_q_my_queue is an identifier for my_queue when partitioned- -- template_pgmq_q_ (16) + <a max length queue name> (47) = 63- RAISE EXCEPTION 'queue name is too long, maximum length is 47 characters';- END IF;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._belongs_to_pgmq(table_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BOOLEAN;-BEGIN- SELECT EXISTS (- SELECT 1- FROM pg_depend- WHERE refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pgmq')- AND objid = (- SELECT oid- FROM pg_class- WHERE relname = table_name- )- ) INTO result;- RETURN result;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._extension_exists(extension_name TEXT)- RETURNS BOOLEAN- LANGUAGE SQL-AS $$-SELECT EXISTS (- SELECT 1- FROM pg_extension- WHERE extname = extension_name-)-$$;
− database/v1.9.0/05_queue_management.sql
@@ -1,261 +0,0 @@---------------------------------------------------------------- Queue management functions---------------------------------------------------------------CREATE FUNCTION pgmq.create_non_partitioned(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create_unlogged(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- )- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT PRIMARY KEY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- );- $QUERY$,- atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, false, true)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.create(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq.create_non_partitioned(queue_name);-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;-BEGIN- RAISE WARNING 'drop_queue(queue_name, partitioned) is deprecated and will be removed in PGMQ v2.0. Use drop_queue(queue_name) instead';-- PERFORM pgmq.drop_queue(queue_name);-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.drop_queue(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- fq_qtable TEXT := 'pgmq.' || qtable;- atable TEXT := pgmq.format_table_name(queue_name, 'a');- fq_atable TEXT := 'pgmq.' || atable;- partitioned BOOLEAN;-BEGIN- PERFORM pgmq.acquire_queue_lock(queue_name);- EXECUTE FORMAT(- $QUERY$- SELECT is_partitioned FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- ) INTO partitioned;-- -- check if the queue exists- IF NOT EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = qtable and table_schema = 'pgmq'- ) THEN- RAISE NOTICE 'pgmq queue `%` does not exist', queue_name;- RETURN FALSE;- END IF;-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable- );-- IF EXISTS (- SELECT 1- FROM information_schema.tables- WHERE table_name = 'meta' and table_schema = 'pgmq'- ) THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM pgmq.meta WHERE queue_name = %L- $QUERY$,- queue_name- );- END IF;-- IF partitioned THEN- EXECUTE FORMAT(- $QUERY$- DELETE FROM %I.part_config where parent_table in (%L, %L)- $QUERY$,- pgmq._get_pg_partman_schema(), fq_qtable, fq_atable- );- END IF;-- RETURN TRUE;-END;-$$ LANGUAGE plpgsql;---- list queues-CREATE FUNCTION pgmq."list_queues"()-RETURNS SETOF pgmq.queue_record AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.meta;-END-$$ LANGUAGE plpgsql;---- purge queue, deleting all entries in it.-CREATE OR REPLACE FUNCTION pgmq."purge_queue"(queue_name TEXT)-RETURNS BIGINT AS $$-DECLARE- deleted_count INTEGER;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- -- Get the row count before truncating- EXECUTE format('SELECT count(*) FROM pgmq.%I', qtable) INTO deleted_count;-- -- Use TRUNCATE for better performance on large tables- EXECUTE format('TRUNCATE TABLE pgmq.%I', qtable);-- -- Return the number of purged rows- RETURN deleted_count;-END-$$ LANGUAGE plpgsql;---- unassign archive, so it can be kept when a queue is deleted-CREATE FUNCTION pgmq."detach_archive"(queue_name TEXT)-RETURNS VOID AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- RAISE WARNING 'detach_archive(queue_name) is deprecated and is a no-op. It will be removed in PGMQ v2.0. Archive tables are no longer member objects.';-END-$$ LANGUAGE plpgsql;
− database/v1.9.0/06_message_ops.sql
@@ -1,626 +0,0 @@---------------------------------------------------------------- Message operations: send, read, delete, archive, pop, set_vt----------------------------------------------------------------- send: 2 args, no delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with headers-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send: 3 args with delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: 4 args with headers and delay-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send: actual implementation-CREATE FUNCTION pgmq.send(- queue_name TEXT,- msg JSONB,- headers JSONB,- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- VALUES ($1, $2, $3)- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msg, headers;-END;-$$ LANGUAGE plpgsql;---- send_batch: no delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[]-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp());-END;-$$ LANGUAGE plpgsql;---- send_batch: with headers and delay-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- delay INTEGER-)-RETURNS SETOF BIGINT AS $$-BEGIN- RETURN QUERY SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp() + make_interval(secs => delay));-END;-$$ LANGUAGE plpgsql;---- send_batch: actual implementation-CREATE FUNCTION pgmq.send_batch(- queue_name TEXT,- msgs JSONB[],- headers JSONB[],- vt TIMESTAMP WITH TIME ZONE-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- INSERT INTO pgmq.%I (vt, message, headers)- SELECT $1, unnest($2), unnest(coalesce($3, array_fill(NULL::jsonb, array[cardinality($2)])))- RETURNING msg_id;- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING vt, msgs, headers;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;------ read----- reads a number of messages from a queue, setting a visibility timeout on them-CREATE FUNCTION pgmq.read(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- reads a number of messages from a queue, setting a visibility timeout on them----- 5-arg version without conditional filter-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 6th parameter (matches pgmq-hasql expected order)----- This is the overload used by pgmq-hasql: read_with_poll($1,$2,$3,$4,$5,$6)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER,- poll_interval_ms INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- -- If conditional is NULL or empty, don't filter- IF conditional IS NULL OR conditional = '{}'::jsonb THEN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- ELSE- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- END IF;-- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;------ read_with_poll----- 6-arg version with conditional as 4th parameter (legacy order)-CREATE FUNCTION pgmq.read_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp() AND message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM cte- WHERE m.msg_id = cte.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- delete--- deletes a message id from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id- $QUERY$,- qtable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- delete--- deletes an array of message ids from the queue permanently-CREATE FUNCTION pgmq.delete(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id- $QUERY$,- qtable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- archive--- removes a message from the queue, and sends it to the archive, where its--- temporary durability is lost and will be deleted on the next archive expiration--- job.-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_id BIGINT-)-RETURNS BOOLEAN AS $$-DECLARE- sql TEXT;- result BIGINT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = $1- RETURNING msg_id, vt, read_ct, enqueued_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- EXECUTE sql USING msg_id INTO result;- RETURN NOT (result IS NULL);-END;-$$ LANGUAGE plpgsql;---- archive--- removes an array of message ids from the queue, and sends it to the archive,--- where its temporary durability is lost and will be deleted on the next archive--- expiration job.-CREATE FUNCTION pgmq.archive(- queue_name TEXT,- msg_ids BIGINT[]-)-RETURNS SETOF BIGINT AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- sql := FORMAT(- $QUERY$- WITH archived AS (- DELETE FROM pgmq.%I- WHERE msg_id = ANY($1)- RETURNING msg_id, vt, read_ct, enqueued_at, message, headers- )- INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, message, headers)- SELECT msg_id, vt, read_ct, enqueued_at, message, headers- FROM archived- RETURNING msg_id;- $QUERY$,- qtable, atable- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;---- pop messages from queue (atomic read + delete)--- qty parameter added in pgmq 1.7.0-CREATE FUNCTION pgmq.pop(- queue_name TEXT,- qty INTEGER DEFAULT 1-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- )- DELETE from pgmq.%I- WHERE msg_id IN (select msg_id from cte)- RETURNING *;- $QUERY$,- qtable, qtable- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- Sets vt of a single message, returns it-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_id BIGINT,- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = %L- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset), msg_id- );- RETURN QUERY EXECUTE sql;-END;-$$ LANGUAGE plpgsql;---- Batch set_vt for multiple messages (pgmq 1.8.0+)-CREATE FUNCTION pgmq.set_vt(- queue_name TEXT,- msg_ids BIGINT[],- vt_offset INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- UPDATE pgmq.%I- SET vt = (clock_timestamp() + %L)- WHERE msg_id = ANY($1)- RETURNING *;- $QUERY$,- qtable, make_interval(secs => vt_offset)- );- RETURN QUERY EXECUTE sql USING msg_ids;-END;-$$ LANGUAGE plpgsql;
− database/v1.9.0/07_metrics.sql
@@ -1,83 +0,0 @@---------------------------------------------------------------- Metrics functions---------------------------------------------------------------CREATE FUNCTION pgmq.metrics(queue_name TEXT)-RETURNS pgmq.metrics_result AS $$-DECLARE- result_row pgmq.metrics_result;- query TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- query := FORMAT(- $QUERY$- WITH q_summary AS (- SELECT- count(*) as queue_length,- EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int as newest_msg_age_sec,- EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int as oldest_msg_age_sec,- NOW() as scrape_time,- count(*) FILTER (WHERE vt <= NOW()) AS queue_visible_length- FROM pgmq.%I- ),- all_metrics AS (- SELECT CASE- WHEN is_partitioned THEN- %L || '_part_' || pgmq._get_partition_col(%L)- ELSE- %L- END as queue_name- FROM pgmq.meta- WHERE meta.queue_name = %L- )- SELECT- m.queue_name,- q_summary.queue_length,- q_summary.newest_msg_age_sec,- q_summary.oldest_msg_age_sec,- pgmq._get_pg_stat_get_xact_tuples_inserted(%L)::bigint,- q_summary.scrape_time,- q_summary.queue_visible_length- FROM q_summary- CROSS JOIN all_metrics m- $QUERY$,- qtable, queue_name, queue_name, queue_name, queue_name, 'pgmq.' || qtable- );- EXECUTE query INTO result_row;- RETURN result_row;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq.metrics_all()-RETURNS SETOF pgmq.metrics_result AS $$-DECLARE- row_name RECORD;- result_row pgmq.metrics_result;-BEGIN- FOR row_name IN SELECT queue_name FROM pgmq.meta LOOP- result_row := pgmq.metrics(row_name.queue_name);- RETURN NEXT result_row;- END LOOP;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_pg_stat_get_xact_tuples_inserted(table_name TEXT)-RETURNS bigint AS $$-DECLARE- result bigint;-BEGIN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = table_name;-- IF result IS NULL THEN- SELECT pg_stat_get_xact_tuples_inserted(oid)- INTO result- FROM pg_class- WHERE relname = SUBSTRING(table_name FROM 6); -- Remove 'pgmq.' prefix (5 chars + 1 for the dot)- END IF;-- RETURN COALESCE(result, 0);-END;-$$ LANGUAGE plpgsql;
− database/v1.9.0/08_partitioning.sql
@@ -1,263 +0,0 @@---------------------------------------------------------------- Partitioning support functions---------------------------------------------------------------CREATE FUNCTION pgmq._get_pg_partman_schema()-RETURNS TEXT AS $$- SELECT extnamespace::regnamespace::text- FROM pg_extension- WHERE extname = 'pg_partman';-$$ LANGUAGE sql STABLE;--CREATE FUNCTION pgmq._get_partition_col(queue_name TEXT)-RETURNS TEXT AS $$-DECLARE- _partition_col TEXT;- _table_name TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- SELECT part_config.partition_interval INTO _partition_col- FROM pg_class- JOIN pg_namespace ON pg_class.relnamespace = pg_namespace.oid- JOIN (SELECT parent_table, partition_interval FROM pgmq._get_partman_config()) AS part_config- ON part_config.parent_table = pg_namespace.nspname || '.' || pg_class.relname- WHERE pg_class.relname = _table_name- AND pg_namespace.nspname = 'pgmq';-- RETURN _partition_col;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._get_partman_config()-RETURNS TABLE (parent_table text, partition_interval text) AS $$-DECLARE- schema_name TEXT;-BEGIN- SELECT pgmq._get_pg_partman_schema() INTO schema_name;- IF schema_name IS NULL THEN- RETURN QUERY SELECT NULL::text, NULL::text WHERE FALSE;- RETURN;- END IF;- RETURN QUERY EXECUTE FORMAT(- $QUERY$- SELECT parent_table, partition_interval- FROM %I.part_config- $QUERY$,- schema_name- );-END;-$$ LANGUAGE plpgsql;---- partitioned, with partition interval-CREATE FUNCTION pgmq.create_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000'-)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- qtable_seq TEXT := qtable || '_msg_id_seq';- atable TEXT := pgmq.format_table_name(queue_name, 'a');- atable_seq TEXT := atable || '_msg_id_seq';-BEGIN- PERFORM pgmq.validate_queue_name(queue_name);- PERFORM pgmq.acquire_queue_lock(queue_name);-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT GENERATED ALWAYS AS IDENTITY,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id)- $QUERY$,- qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), qtable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := 1- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, qtable- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);- $QUERY$,- qtable || '_vt_idx', qtable- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)- VALUES (%L, true, false)- ON CONFLICT- DO NOTHING;- $QUERY$,- queue_name- );--END;-$$ LANGUAGE plpgsql;---- convert_archive_partitioned-CREATE FUNCTION pgmq.convert_archive_partitioned(- queue_name TEXT,- partition_interval TEXT DEFAULT '10000',- retention_interval TEXT DEFAULT '100000',- leading_partition INT DEFAULT 10-)-RETURNS void AS $$-DECLARE- atable TEXT := pgmq.format_table_name(queue_name, 'a');-BEGIN- EXECUTE FORMAT(- $QUERY$- ALTER TABLE IF EXISTS pgmq.%I RENAME TO %I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE TABLE IF NOT EXISTS pgmq.%I (- msg_id BIGINT NOT NULL,- read_ct INT DEFAULT 0 NOT NULL,- enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,- vt TIMESTAMP WITH TIME ZONE NOT NULL,- message JSONB,- headers JSONB- ) PARTITION BY RANGE (msg_id);- $QUERY$,- atable- );-- IF NOT pgmq._extension_exists('pg_partman') THEN- RAISE EXCEPTION 'pg_partman is required for partitioned queues';- END IF;-- EXECUTE FORMAT(- $QUERY$- SELECT %I.create_parent(- p_parent_table := 'pgmq.%I',- p_control := 'msg_id',- p_type := 'range',- p_interval := %L,- p_premake := %L- )- $QUERY$,- pgmq._get_pg_partman_schema(), atable, partition_interval, leading_partition- );-- EXECUTE FORMAT(- $QUERY$- UPDATE %I.part_config- SET- retention = %L,- retention_keep_table = false,- retention_keep_index = true,- automatic_maintenance = 'on'- WHERE parent_table = 'pgmq.%I';- $QUERY$,- pgmq._get_pg_partman_schema(), retention_interval, atable- );-- EXECUTE FORMAT(- $QUERY$- INSERT INTO pgmq.%I (msg_id, read_ct, enqueued_at, archived_at, vt, message, headers)- SELECT msg_id, read_ct, enqueued_at, archived_at, vt, message, headers FROM pgmq.%I- $QUERY$,- atable, atable || '_old'- );-- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);- $QUERY$,- 'archived_at_idx_' || queue_name, atable- );-- EXECUTE FORMAT(- $QUERY$- DROP TABLE IF EXISTS pgmq.%I- $QUERY$,- atable || '_old'- );-END;-$$ LANGUAGE plpgsql;
− database/v1.9.0/09_notifications.sql
@@ -1,103 +0,0 @@---------------------------------------------------------------- Notification functions---------------------------------------------------------------CREATE FUNCTION pgmq._should_notify(queue_name TEXT)-RETURNS BOOLEAN AS $$-DECLARE- should_notify BOOLEAN;- throttle_ms INTEGER;- last_notified TIMESTAMP WITH TIME ZONE;-BEGIN- -- Try to get throttle settings- SELECT throttle_interval_ms, last_notified_at- INTO throttle_ms, last_notified- FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name- FOR UPDATE SKIP LOCKED;-- -- If no row found, assume notifications are enabled without throttling- IF throttle_ms IS NULL THEN- RETURN TRUE;- END IF;-- -- Check if enough time has passed since last notification- IF clock_timestamp() >= last_notified + make_interval(secs => throttle_ms::numeric / 1000) THEN- -- Update the last notification time- UPDATE pgmq.notify_insert_throttle- SET last_notified_at = clock_timestamp()- WHERE pgmq.notify_insert_throttle.queue_name = _should_notify.queue_name;- RETURN TRUE;- END IF;-- RETURN FALSE;-END;-$$ LANGUAGE plpgsql;--CREATE FUNCTION pgmq._notify_on_insert()-RETURNS TRIGGER AS $$-BEGIN- IF pgmq._should_notify(TG_ARGV[0]) THEN- PERFORM pg_notify(TG_ARGV[0], NEW.msg_id::text);- END IF;- RETURN NEW;-END;-$$ LANGUAGE plpgsql;---- Add trigger to queue table to notify on insert-CREATE FUNCTION pgmq.enable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- channel TEXT := queue_name;- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Create trigger on the queue table- EXECUTE FORMAT(- $QUERY$- CREATE TRIGGER %I- AFTER INSERT ON pgmq.%I- FOR EACH ROW- EXECUTE FUNCTION pgmq._notify_on_insert(%L)- $QUERY$,- trigger_name, qtable, channel- );-- -- Insert throttle tracking row if it doesn't exist- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (queue_name, 0, to_timestamp(0))- ON CONFLICT (queue_name) DO NOTHING;-END;-$$ LANGUAGE plpgsql;---- Remove trigger from queue table-CREATE FUNCTION pgmq.disable_queue_notifications(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- trigger_name TEXT := 'notify_insert_' || qtable;-BEGIN- -- Drop trigger if it exists- EXECUTE FORMAT(- $QUERY$- DROP TRIGGER IF EXISTS %I ON pgmq.%I- $QUERY$,- trigger_name, qtable- );-- -- Remove throttle tracking row- DELETE FROM pgmq.notify_insert_throttle- WHERE pgmq.notify_insert_throttle.queue_name = disable_queue_notifications.queue_name;-END;-$$ LANGUAGE plpgsql;---- Set throttle interval for queue notifications-CREATE FUNCTION pgmq.set_notification_throttle(queue_name TEXT, throttle_interval_ms INTEGER)-RETURNS void AS $$-BEGIN- INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms, last_notified_at)- VALUES (queue_name, throttle_interval_ms, to_timestamp(0))- ON CONFLICT (queue_name) DO UPDATE- SET throttle_interval_ms = EXCLUDED.throttle_interval_ms;-END;-$$ LANGUAGE plpgsql;
− database/v1.9.0/10_fifo.sql
@@ -1,564 +0,0 @@---------------------------------------------------------------- FIFO read functions (for pgmq 1.8.0+ and 1.9.0+)----------------------------------------------------------------- read_fifo: reads messages in strict FIFO order--- This function ensures messages are read in strict FIFO order by:--- 1. Ordering by msg_id ASC (not just using SKIP LOCKED)--- 2. Only returning messages that don't skip any earlier pending messages-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_fifo with conditional filter-CREATE FUNCTION pgmq.read_fifo(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty, conditional;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll: reads messages in strict FIFO order with polling-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;---- read_fifo_with_poll with conditional filter-CREATE FUNCTION pgmq.read_fifo_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- conditional JSONB,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF clock_timestamp() >= stop_at THEN- RETURN;- END IF;-- sql := FORMAT(- $QUERY$- WITH cte AS- (- SELECT msg_id, vt AS current_vt- FROM pgmq.%I- WHERE message @> $2- ORDER BY msg_id ASC- LIMIT $1- FOR UPDATE SKIP LOCKED- ),- eligible AS (- SELECT c.msg_id- FROM cte c- WHERE c.msg_id = (- SELECT MIN(msg_id)- FROM pgmq.%I- WHERE vt <= clock_timestamp()- AND message @> $2- )- OR NOT EXISTS (- SELECT 1- FROM pgmq.%I q- WHERE q.msg_id < c.msg_id- AND q.vt <= clock_timestamp()- AND q.message @> $2- AND q.msg_id NOT IN (SELECT msg_id FROM cte)- )- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM eligible e- WHERE m.msg_id = e.msg_id- AND m.vt <= clock_timestamp()- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, make_interval(secs => vt)- );-- FOR r IN- EXECUTE sql USING qty, conditional- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- FIFO index functions (pgmq 1.8.0+)----------------------------------------------------------------- _create_fifo_index_if_not_exists--- internal function to create GIN index on headers for better FIFO performance-CREATE OR REPLACE FUNCTION pgmq._create_fifo_index_if_not_exists(queue_name TEXT)-RETURNS void AS $$-DECLARE- qtable TEXT := pgmq.format_table_name(queue_name, 'q');- index_name TEXT := qtable || '_fifo_idx';-BEGIN- -- Create GIN index on headers for efficient FIFO key lookups- EXECUTE FORMAT(- $QUERY$- CREATE INDEX IF NOT EXISTS %I ON pgmq.%I USING GIN (headers);- $QUERY$,- index_name, qtable- );-END;-$$ LANGUAGE plpgsql;---- create_fifo_index--- creates a GIN index on the headers column to improve FIFO read performance-CREATE FUNCTION pgmq.create_fifo_index(queue_name TEXT)-RETURNS void AS $$-BEGIN- PERFORM pgmq._create_fifo_index_if_not_exists(queue_name);-END;-$$ LANGUAGE plpgsql;---- create_fifo_indexes_all--- creates FIFO indexes on all existing queues-CREATE FUNCTION pgmq.create_fifo_indexes_all()-RETURNS void AS $$-DECLARE- queue_record RECORD;-BEGIN- FOR queue_record IN SELECT queue_name FROM pgmq.meta LOOP- PERFORM pgmq.create_fifo_index(queue_record.queue_name);- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- read_grouped functions (pgmq 1.8.0+)--- AWS SQS FIFO-style batch retrieval behavior----------------------------------------------------------------- read_grouped--- reads messages with AWS SQS FIFO-style batch retrieval behavior--- attempts to return as many messages as possible from the same message group-CREATE FUNCTION pgmq.read_grouped(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Find the minimum msg_id for each FIFO group that's ready to be processed- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,- MIN(msg_id) as min_msg_id- FROM pgmq.%I- WHERE vt <= clock_timestamp()- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- locked_groups AS (- -- Lock the first available message in each FIFO group- SELECT- m.msg_id,- fg.fifo_key- FROM pgmq.%I m- INNER JOIN fifo_groups fg ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key- AND m.msg_id = fg.min_msg_id- WHERE m.vt <= clock_timestamp()- ORDER BY m.msg_id ASC- FOR UPDATE SKIP LOCKED- ),- group_priorities AS (- -- Assign priority to groups based on their oldest message- SELECT- fifo_key,- msg_id as min_msg_id,- ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority- FROM locked_groups- ),- available_messages AS (- -- Get messages prioritizing filling batch from earliest group first- SELECT- m.msg_id,- gp.group_priority,- ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY m.msg_id) as msg_rank_in_group- FROM pgmq.%I m- INNER JOIN group_priorities gp ON- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= gp.min_msg_id -- Only messages from min_msg_id onwards in each group- AND NOT EXISTS (- -- Ensure no earlier message in this group is currently being processed- SELECT 1- FROM pgmq.%I m2- WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') =- COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group')- AND m2.vt > clock_timestamp()- AND m2.msg_id < m.msg_id- )- ),- batch_selection AS (- -- Select messages to fill batch, prioritizing earliest group- SELECT- msg_id,- ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank- FROM available_messages- ),- selected_messages AS (- -- Limit to requested quantity- SELECT msg_id- FROM batch_selection- WHERE overall_rank <= $1- ORDER BY msg_id- FOR UPDATE SKIP LOCKED- )- UPDATE pgmq.%I m- SET- vt = clock_timestamp() + %L,- read_ct = read_ct + 1- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_with_poll--- reads messages with AWS SQS FIFO-style batch retrieval behavior, with polling support-CREATE FUNCTION pgmq.read_grouped_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;----------------------------------------------------------------- Round-robin FIFO functions (pgmq 1.9.0+)----------------------------------------------------------------- read_grouped_rr (round-robin)--- reads messages while preserving FIFO within groups and interleaving across groups (layered round-robin)-CREATE FUNCTION pgmq.read_grouped_rr(- queue_name TEXT,- vt INTEGER,- qty INTEGER-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- sql TEXT;- qtable TEXT := pgmq.format_table_name(queue_name, 'q');-BEGIN- sql := FORMAT(- $QUERY$- WITH fifo_groups AS (- -- Determine the absolute head (oldest) message id per FIFO group, regardless of visibility- SELECT- COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') AS fifo_key,- MIN(msg_id) AS head_msg_id- FROM pgmq.%1$I- GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')- ),- eligible_groups AS (- -- Only groups whose head message is currently visible- -- Acquire a transaction-level advisory lock per group to prevent concurrent selection- SELECT- g.fifo_key,- g.head_msg_id,- ROW_NUMBER() OVER (ORDER BY g.head_msg_id) AS group_priority- FROM fifo_groups g- JOIN pgmq.%2$I h ON h.msg_id = g.head_msg_id- WHERE h.vt <= clock_timestamp()- AND pg_try_advisory_xact_lock(pg_catalog.hashtextextended(g.fifo_key, 0))- ),- available_messages AS (- -- All currently visible messages starting at the head for each eligible group- SELECT- m.msg_id,- eg.group_priority,- ROW_NUMBER() OVER (- PARTITION BY eg.fifo_key- ORDER BY m.msg_id- ) AS msg_rank_in_group- FROM pgmq.%3$I m- JOIN eligible_groups eg- ON COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = eg.fifo_key- WHERE m.vt <= clock_timestamp()- AND m.msg_id >= eg.head_msg_id- ),- ordered_messages AS (- -- Layered round-robin: take rank 1 of all groups by group_priority, then rank 2, etc.- -- Assign selection order before locking- SELECT msg_id, ROW_NUMBER() OVER (ORDER BY msg_rank_in_group, group_priority) as selection_order- FROM available_messages- ),- selected_messages AS (- -- Lock the messages in the correct order, preserving selection_order- SELECT om.msg_id, om.selection_order- FROM ordered_messages om- JOIN pgmq.%4$I m ON m.msg_id = om.msg_id- WHERE om.selection_order <= $1- ORDER BY om.selection_order- FOR UPDATE OF m SKIP LOCKED- ),- updated_messages AS (- UPDATE pgmq.%5$I m- SET- vt = clock_timestamp() + %6$L,- read_ct = read_ct + 1- FROM selected_messages sm- WHERE m.msg_id = sm.msg_id- AND m.vt <= clock_timestamp() -- final guard to avoid duplicate reads under races- RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message, m.headers, sm.selection_order- )- SELECT msg_id, read_ct, enqueued_at, vt, message, headers- FROM updated_messages- ORDER BY selection_order;- $QUERY$,- qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)- );- RETURN QUERY EXECUTE sql USING qty;-END;-$$ LANGUAGE plpgsql;---- read_grouped_rr_with_poll--- reads messages using round-robin layering across groups, with polling support-CREATE FUNCTION pgmq.read_grouped_rr_with_poll(- queue_name TEXT,- vt INTEGER,- qty INTEGER,- max_poll_seconds INTEGER DEFAULT 5,- poll_interval_ms INTEGER DEFAULT 100-)-RETURNS SETOF pgmq.message_record AS $$-DECLARE- r pgmq.message_record;- stop_at TIMESTAMP;-BEGIN- stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);- LOOP- IF (SELECT clock_timestamp() >= stop_at) THEN- RETURN;- END IF;-- FOR r IN- SELECT * FROM pgmq.read_grouped_rr(queue_name, vt, qty)- LOOP- RETURN NEXT r;- END LOOP;- IF FOUND THEN- RETURN;- ELSE- PERFORM pg_sleep(poll_interval_ms::numeric / 1000);- END IF;- END LOOP;-END;-$$ LANGUAGE plpgsql;
pgmq-migration.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: pgmq-migration-version: 0.1.1.0+version: 0.1.2.0 synopsis: PGMQ schema migrations without PostgreSQL extension description: Installs the PGMQ schema into PostgreSQL without requiring the pgmq extension.@@ -14,7 +14,10 @@ category: Database build-type: Simple extra-doc-files: CHANGELOG.md-extra-source-files: database/**/*.sql+extra-source-files:+ vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql+ vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql+ vendor/pgmq/pgmq-extension/sql/pgmq.sql common warnings ghc-options:@@ -28,11 +31,9 @@ exposed-modules: Pgmq.Migration Pgmq.Migration.Migrations- Pgmq.Migration.Migrations.V1_10_0- Pgmq.Migration.Migrations.V1_10_0_to_V1_11_0+ Pgmq.Migration.Migrations.V1_10_0_to_V1_10_1+ Pgmq.Migration.Migrations.V1_10_1_to_V1_11_0 Pgmq.Migration.Migrations.V1_11_0- Pgmq.Migration.Migrations.V1_9_0- Pgmq.Migration.Migrations.V1_9_0_to_V1_10_0 Pgmq.Migration.Sessions Pgmq.Migration.Statements Pgmq.Migration.Transactions
src/Pgmq/Migration.hs view
@@ -25,7 +25,7 @@ -- -- == Upgrading Existing Installations ----- For projects that previously installed PGMQ via this package (e.g., at v1.9.0),+-- For projects that previously installed PGMQ via this package (e.g., at v1.10.0), -- use 'upgrade' to apply only the incremental changes: -- -- @@@ -91,7 +91,7 @@ -- | Incremental upgrade migrations. -- -- Use this to upgrade existing installations that were set up via this package.--- Contains only the delta migrations (e.g., v1.9.0 -> v1.10.0).+-- Contains only the delta migrations (e.g., v1.10.0 -> v1.10.1 -> v1.11.0). upgradeMigrations :: [MigrationCommand] upgradeMigrations = Migrations.upgradeMigrations @@ -107,7 +107,7 @@ -- | Run upgrade migrations for existing installations. -- -- This function applies only the incremental migrations needed to upgrade--- from a previous version installed via this package (e.g., v1.9.0 -> v1.10.0).+-- from a previous version installed via this package (e.g., v1.10.0 -> v1.10.1 -> v1.11.0). -- -- It is idempotent and safe to run on any database - migrations that have -- already been applied will be skipped.
src/Pgmq/Migration/Migrations.hs view
@@ -21,13 +21,23 @@ -- @ -- import Pgmq.Migration.Migrations (upgradeMigrations) ----- -- Apply only upgrade migrations (v1.9.0 -> v1.10.0 -> v1.11.0, etc.)+-- -- Apply only upgrade migrations (v1.10.0 -> v1.10.1 -> v1.11.0) -- runMigrations conn upgradeMigrations -- @ -- -- The hasql-migration library tracks which migrations have been applied,--- so running 'upgradeMigrations' on a database with v1.9.0 installed will+-- so running 'upgradeMigrations' on a database with v1.10.0 installed will -- only apply the delta migrations needed to reach the current version.+--+-- == Compatibility Note+--+-- Existing deployments that ran the old hand-written upgrade migrations+-- (e.g., @pgmq_v1.9.0_to_v1.10.0_upgrade@, @pgmq_v1.10.0_to_v1.11.0_upgrade@)+-- have those names recorded in @schema_migrations@. The vendored migrations use+-- different names, so @hasql-migration@ will treat them as new, unapplied migrations.+-- This is correct — the vendored migrations re-apply upstream function definitions+-- using @CREATE OR REPLACE FUNCTION@ and @IF NOT EXISTS@ guards, which are safe+-- to re-run. module Pgmq.Migration.Migrations ( version, migrations,@@ -36,9 +46,9 @@ where import Hasql.Migration (MigrationCommand (..))-import Pgmq.Migration.Migrations.V1_10_0_to_V1_11_0 qualified as V1_10_0_to_V1_11_0+import Pgmq.Migration.Migrations.V1_10_0_to_V1_10_1 qualified as V1_10_0_to_V1_10_1+import Pgmq.Migration.Migrations.V1_10_1_to_V1_11_0 qualified as V1_10_1_to_V1_11_0 import Pgmq.Migration.Migrations.V1_11_0 qualified as V1_11_0-import Pgmq.Migration.Migrations.V1_9_0_to_V1_10_0 qualified as V1_9_0_to_V1_10_0 -- | Current version of the PGMQ schema version :: String@@ -53,7 +63,7 @@ -- | Incremental upgrade migrations. -- -- Use this to upgrade existing installations that were set up via this package.--- Contains only the delta migrations (e.g., v1.9.0 -> v1.10.0 -> v1.11.0).+-- Contains only the delta migrations (v1.10.0 -> v1.10.1 -> v1.11.0). -- -- The hasql-migration library will skip migrations that have already been applied, -- so this is safe to run on any database regardless of its current version.@@ -61,5 +71,5 @@ upgradeMigrations = [ MigrationInitialization ]- ++ V1_9_0_to_V1_10_0.migrations- ++ V1_10_0_to_V1_11_0.migrations+ ++ V1_10_0_to_V1_10_1.migrations+ ++ V1_10_1_to_V1_11_0.migrations
− src/Pgmq/Migration/Migrations/V1_10_0.hs
@@ -1,63 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}---- | PGMQ v1.10.0 migrations--- Main change: Added last_read_at column to track when messages were last read-module Pgmq.Migration.Migrations.V1_10_0- ( version,- migrations,- )-where--import Data.ByteString (ByteString)-import Data.FileEmbed (embedFile)-import Hasql.Migration (MigrationCommand (..))---- | Version string for this migration set-version :: String-version = "v1.10.0"---- | All migration commands for v1.10.0-migrations :: [MigrationCommand]-migrations =- [ MigrationInitialization,- MigrationScript "pgmq_v1.10.0_01_schema" schema,- MigrationScript "pgmq_v1.10.0_02_tables" tables,- MigrationScript "pgmq_v1.10.0_03_types" types,- MigrationScript "pgmq_v1.10.0_04_core_functions" coreFunctions,- MigrationScript "pgmq_v1.10.0_05_queue_management" queueManagement,- MigrationScript "pgmq_v1.10.0_06_message_ops" messageOps,- MigrationScript "pgmq_v1.10.0_07_metrics" metrics,- MigrationScript "pgmq_v1.10.0_08_partitioning" partitioning,- MigrationScript "pgmq_v1.10.0_09_notifications" notifications,- MigrationScript "pgmq_v1.10.0_10_fifo" fifo- ]--schema :: ByteString-schema = $(embedFile "database/v1.10.0/01_schema.sql")--tables :: ByteString-tables = $(embedFile "database/v1.10.0/02_tables.sql")--types :: ByteString-types = $(embedFile "database/v1.10.0/03_types.sql")--coreFunctions :: ByteString-coreFunctions = $(embedFile "database/v1.10.0/04_core_functions.sql")--queueManagement :: ByteString-queueManagement = $(embedFile "database/v1.10.0/05_queue_management.sql")--messageOps :: ByteString-messageOps = $(embedFile "database/v1.10.0/06_message_ops.sql")--metrics :: ByteString-metrics = $(embedFile "database/v1.10.0/07_metrics.sql")--partitioning :: ByteString-partitioning = $(embedFile "database/v1.10.0/08_partitioning.sql")--notifications :: ByteString-notifications = $(embedFile "database/v1.10.0/09_notifications.sql")--fifo :: ByteString-fifo = $(embedFile "database/v1.10.0/10_fifo.sql")
+ src/Pgmq/Migration/Migrations/V1_10_0_to_V1_10_1.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++-- | PGMQ v1.10.0 to v1.10.1 migration+--+-- Embeds the upstream pgmq--1.10.0--1.10.1.sql migration directly.+-- Changes: Function signature refinements, read_grouped LATERAL join optimization.+module Pgmq.Migration.Migrations.V1_10_0_to_V1_10_1+ ( migrations,+ )+where++import Data.FileEmbed (embedFile)+import Hasql.Migration (MigrationCommand (..))++-- | Migration commands to upgrade from v1.10.0 to v1.10.1+migrations :: [MigrationCommand]+migrations =+ [ MigrationScript+ "pgmq_v1.10.0_to_v1.10.1"+ $(embedFile "vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql")+ ]
− src/Pgmq/Migration/Migrations/V1_10_0_to_V1_11_0.hs
@@ -1,22 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}---- | PGMQ v1.10.0 to v1.11.0 migration--- Changes: AMQP-like topic-based routing, notification management functions,--- batch validation helpers, read_grouped fix-module Pgmq.Migration.Migrations.V1_10_0_to_V1_11_0- ( migrations,- )-where--import Data.ByteString (ByteString)-import Data.FileEmbed (embedFile)-import Hasql.Migration (MigrationCommand (..))---- | Migration commands to upgrade from v1.10.0 to v1.11.0-migrations :: [MigrationCommand]-migrations =- [ MigrationScript "pgmq_v1.10.0_to_v1.11.0_upgrade" upgrade- ]--upgrade :: ByteString-upgrade = $(embedFile "database/migrations/v1.10.0_to_v1.11.0.sql")
+ src/Pgmq/Migration/Migrations/V1_10_1_to_V1_11_0.hs view
@@ -0,0 +1,21 @@+{-# LANGUAGE TemplateHaskell #-}++-- | PGMQ v1.10.1 to v1.11.0 migration+--+-- Embeds the upstream pgmq--1.10.1--1.11.0.sql migration directly.+-- Changes: Topic routing, batch validation, notification throttle management.+module Pgmq.Migration.Migrations.V1_10_1_to_V1_11_0+ ( migrations,+ )+where++import Data.FileEmbed (embedFile)+import Hasql.Migration (MigrationCommand (..))++-- | Migration commands to upgrade from v1.10.1 to v1.11.0+migrations :: [MigrationCommand]+migrations =+ [ MigrationScript+ "pgmq_v1.10.1_to_v1.11.0"+ $(embedFile "vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql")+ ]
src/Pgmq/Migration/Migrations/V1_11_0.hs view
@@ -1,14 +1,15 @@ {-# LANGUAGE TemplateHaskell #-} -- | PGMQ v1.11.0 migrations--- Main change: AMQP-like topic-based routing, notification management functions+--+-- Embeds the upstream pgmq.sql directly from the vendored git subtree.+-- This is the single source of truth for the full PGMQ schema. module Pgmq.Migration.Migrations.V1_11_0 ( version, migrations, ) where -import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Hasql.Migration (MigrationCommand (..)) @@ -17,51 +18,12 @@ version = "v1.11.0" -- | All migration commands for v1.11.0+--+-- Uses the upstream pgmq.sql directly. The file is already ordered correctly+-- (types before functions, tables before references) and PostgreSQL executes+-- it as one transaction. migrations :: [MigrationCommand] migrations = [ MigrationInitialization,- MigrationScript "pgmq_v1.11.0_01_schema" schema,- MigrationScript "pgmq_v1.11.0_02_tables" tables,- MigrationScript "pgmq_v1.11.0_03_types" types,- MigrationScript "pgmq_v1.11.0_04_core_functions" coreFunctions,- MigrationScript "pgmq_v1.11.0_05_queue_management" queueManagement,- MigrationScript "pgmq_v1.11.0_06_message_ops" messageOps,- MigrationScript "pgmq_v1.11.0_07_metrics" metrics,- MigrationScript "pgmq_v1.11.0_08_partitioning" partitioning,- MigrationScript "pgmq_v1.11.0_09_notifications" notifications,- MigrationScript "pgmq_v1.11.0_10_fifo" fifo,- MigrationScript "pgmq_v1.11.0_11_topics" topics+ MigrationScript "pgmq_v1.11.0" $(embedFile "vendor/pgmq/pgmq-extension/sql/pgmq.sql") ]--schema :: ByteString-schema = $(embedFile "database/v1.11.0/01_schema.sql")--tables :: ByteString-tables = $(embedFile "database/v1.11.0/02_tables.sql")--types :: ByteString-types = $(embedFile "database/v1.11.0/03_types.sql")--coreFunctions :: ByteString-coreFunctions = $(embedFile "database/v1.11.0/04_core_functions.sql")--queueManagement :: ByteString-queueManagement = $(embedFile "database/v1.11.0/05_queue_management.sql")--messageOps :: ByteString-messageOps = $(embedFile "database/v1.11.0/06_message_ops.sql")--metrics :: ByteString-metrics = $(embedFile "database/v1.11.0/07_metrics.sql")--partitioning :: ByteString-partitioning = $(embedFile "database/v1.11.0/08_partitioning.sql")--notifications :: ByteString-notifications = $(embedFile "database/v1.11.0/09_notifications.sql")--fifo :: ByteString-fifo = $(embedFile "database/v1.11.0/10_fifo.sql")--topics :: ByteString-topics = $(embedFile "database/v1.11.0/11_topics.sql")
− src/Pgmq/Migration/Migrations/V1_9_0.hs
@@ -1,62 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}---- | PGMQ v1.9.0 migrations-module Pgmq.Migration.Migrations.V1_9_0- ( version,- migrations,- )-where--import Data.ByteString (ByteString)-import Data.FileEmbed (embedFile)-import Hasql.Migration (MigrationCommand (..))---- | Version string for this migration set-version :: String-version = "v1.9.0"---- | All migration commands for v1.9.0-migrations :: [MigrationCommand]-migrations =- [ MigrationInitialization,- MigrationScript "pgmq_v1.9.0_01_schema" schema,- MigrationScript "pgmq_v1.9.0_02_tables" tables,- MigrationScript "pgmq_v1.9.0_03_types" types,- MigrationScript "pgmq_v1.9.0_04_core_functions" coreFunctions,- MigrationScript "pgmq_v1.9.0_05_queue_management" queueManagement,- MigrationScript "pgmq_v1.9.0_06_message_ops" messageOps,- MigrationScript "pgmq_v1.9.0_07_metrics" metrics,- MigrationScript "pgmq_v1.9.0_08_partitioning" partitioning,- MigrationScript "pgmq_v1.9.0_09_notifications" notifications,- MigrationScript "pgmq_v1.9.0_10_fifo" fifo- ]--schema :: ByteString-schema = $(embedFile "database/v1.9.0/01_schema.sql")--tables :: ByteString-tables = $(embedFile "database/v1.9.0/02_tables.sql")--types :: ByteString-types = $(embedFile "database/v1.9.0/03_types.sql")--coreFunctions :: ByteString-coreFunctions = $(embedFile "database/v1.9.0/04_core_functions.sql")--queueManagement :: ByteString-queueManagement = $(embedFile "database/v1.9.0/05_queue_management.sql")--messageOps :: ByteString-messageOps = $(embedFile "database/v1.9.0/06_message_ops.sql")--metrics :: ByteString-metrics = $(embedFile "database/v1.9.0/07_metrics.sql")--partitioning :: ByteString-partitioning = $(embedFile "database/v1.9.0/08_partitioning.sql")--notifications :: ByteString-notifications = $(embedFile "database/v1.9.0/09_notifications.sql")--fifo :: ByteString-fifo = $(embedFile "database/v1.9.0/10_fifo.sql")
− src/Pgmq/Migration/Migrations/V1_9_0_to_V1_10_0.hs
@@ -1,21 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}---- | PGMQ v1.9.0 to v1.10.0 migration--- Changes: Added last_read_at column to track when messages were last read-module Pgmq.Migration.Migrations.V1_9_0_to_V1_10_0- ( migrations,- )-where--import Data.ByteString (ByteString)-import Data.FileEmbed (embedFile)-import Hasql.Migration (MigrationCommand (..))---- | Migration commands to upgrade from v1.9.0 to v1.10.0-migrations :: [MigrationCommand]-migrations =- [ MigrationScript "pgmq_v1.9.0_to_v1.10.0_upgrade" upgrade- ]--upgrade :: ByteString-upgrade = $(embedFile "database/migrations/v1.9.0_to_v1.10.0.sql")
test/Main.hs view
@@ -2,24 +2,18 @@ module Main (main) where -import Control.Monad (foldM)-import Data.List (isInfixOf) import EphemeralPg- ( StartError,- connectionSettings,+ ( connectionSettings, withCached, ) import Hasql.Connection qualified as Connection import Hasql.Decoders qualified as Decoders import Hasql.Encoders qualified as Encoders-import Hasql.Migration (MigrationCommand, MigrationError) import Hasql.Session (Session) import Hasql.Session qualified as Session import Hasql.Statement (preparable) import Hasql.Statement qualified as Statement import Pgmq.Migration qualified as Migration-import Pgmq.Migration.Migrations.V1_9_0 qualified as V1_9_0-import Pgmq.Migration.Sessions qualified as Sessions import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=)) @@ -49,9 +43,8 @@ ], testGroup "upgrade"- [ testCase "upgrade from v1.9.0 succeeds" (testUpgradeFromV1_9_0 conn),- testCase "upgrade is idempotent" (testUpgradeIdempotent conn),- testCase "upgrade adds last_read_at column" (testUpgradeAddsLastReadAt conn)+ [ testCase "upgrade is idempotent" (testUpgradeIdempotent conn),+ testCase "upgrade after migrate succeeds" (testUpgradeAfterMigrate conn) ] ] @@ -123,57 +116,23 @@ case migrationsResult of Left sessionErr -> assertFailure $ "Session error: " <> show sessionErr Right appliedMigrations -> do- -- Should have applied the initialization + 10 SQL migrations- assertBool "Should have applied migrations" (length appliedMigrations >= 10)+ -- Should have applied at least the pgmq_v1.11.0 SQL migration+ assertBool "Should have applied migrations" (length appliedMigrations >= 1) testVersion :: IO () testVersion = Migration.version @?= "v1.11.0" --- | Helper to run a list of migrations-runMigrations :: [MigrationCommand] -> Session (Either MigrationError ())-runMigrations cmds = foldM runIfOk (Right ()) cmds- where- runIfOk :: Either MigrationError () -> MigrationCommand -> Session (Either MigrationError ())- runIfOk (Left err) _ = pure (Left err)- runIfOk (Right ()) cmd = Sessions.runMigrationSession cmd---- | Install v1.9.0 schema-installV1_9_0 :: Session (Either MigrationError ())-installV1_9_0 = runMigrations V1_9_0.migrations--testUpgradeFromV1_9_0 :: Connection.Connection -> IO ()-testUpgradeFromV1_9_0 conn = withCleanDb conn $ \c -> do- -- First install v1.9.0 schema- v190Result <- Connection.use c installV1_9_0- case v190Result of- Left sessionErr -> assertFailure $ "v1.9.0 install session error: " <> show sessionErr- Right (Left migrationErr) -> assertFailure $ "v1.9.0 install migration error: " <> show migrationErr- Right (Right ()) -> pure ()-- -- Now run upgrade- upgradeResult <- Connection.use c Migration.upgrade- case upgradeResult of- Left sessionErr -> assertFailure $ "Upgrade session error: " <> show sessionErr- Right (Left migrationErr) -> assertFailure $ "Upgrade migration error: " <> show migrationErr- Right (Right ()) -> pure ()-- -- Verify upgrade migration was applied- migrationsResult <- Connection.use c Migration.getMigrations- case migrationsResult of- Left sessionErr -> assertFailure $ "getMigrations session error: " <> show sessionErr- Right appliedMigrations -> do- let migrationNames = map show appliedMigrations- hasUpgrade = any ("v1.9.0_to_v1.10.0" `isInfixOf`) migrationNames- assertBool "Should have applied upgrade migration" hasUpgrade-+-- | Run upgrade twice after a fresh install - second run should be a no-op.+-- Upgrade migrations assume the pgmq schema already exists, so we install+-- the base schema first. testUpgradeIdempotent :: Connection.Connection -> IO () testUpgradeIdempotent conn = withCleanDb conn $ \c -> do- -- First install v1.9.0 schema- v190Result <- Connection.use c installV1_9_0- case v190Result of- Left sessionErr -> assertFailure $ "v1.9.0 install session error: " <> show sessionErr- Right (Left migrationErr) -> assertFailure $ "v1.9.0 install migration error: " <> show migrationErr+ -- First install the base schema+ migResult <- Connection.use c Migration.migrate+ case migResult of+ Left sessionErr -> assertFailure $ "Migrate session error: " <> show sessionErr+ Right (Left migrationErr) -> assertFailure $ "Migrate error: " <> show migrationErr Right (Right ()) -> pure () -- Run upgrade first time@@ -190,61 +149,20 @@ Right (Left migrationErr) -> assertFailure $ "Second upgrade migration error: " <> show migrationErr Right (Right ()) -> pure () -testUpgradeAddsLastReadAt :: Connection.Connection -> IO ()-testUpgradeAddsLastReadAt conn = withCleanDb conn $ \c -> do- -- First install v1.9.0 schema- v190Result <- Connection.use c installV1_9_0- case v190Result of- Left sessionErr -> assertFailure $ "v1.9.0 install session error: " <> show sessionErr- Right (Left migrationErr) -> assertFailure $ "v1.9.0 install migration error: " <> show migrationErr+-- | Run migrate then upgrade - tests that CREATE OR REPLACE FUNCTION+-- makes re-applying safe+testUpgradeAfterMigrate :: Connection.Connection -> IO ()+testUpgradeAfterMigrate conn = withCleanDb conn $ \c -> do+ -- First do a fresh install+ migResult <- Connection.use c Migration.migrate+ case migResult of+ Left sessionErr -> assertFailure $ "Migrate session error: " <> show sessionErr+ Right (Left migrationErr) -> assertFailure $ "Migrate error: " <> show migrationErr Right (Right ()) -> pure () - -- Create a queue to test the schema- createQueueResult <- Connection.use c createTestQueue- case createQueueResult of- Left sessionErr -> assertFailure $ "Create queue session error: " <> show sessionErr- Right () -> pure ()-- -- Run upgrade+ -- Now run upgrade - should succeed (migrations are safe to re-apply) upgradeResult <- Connection.use c Migration.upgrade case upgradeResult of Left sessionErr -> assertFailure $ "Upgrade session error: " <> show sessionErr Right (Left migrationErr) -> assertFailure $ "Upgrade migration error: " <> show migrationErr Right (Right ()) -> pure ()-- -- Verify last_read_at column exists in the queue table- checkResult <- Connection.use c checkLastReadAtColumn- case checkResult of- Left sessionErr -> assertFailure $ "Check column session error: " <> show sessionErr- Right hasColumn ->- assertBool "Queue table should have last_read_at column after upgrade" hasColumn---- | Create a test queue using pgmq.create-createTestQueue :: Session ()-createTestQueue = Session.statement () createQueueStmt- where- createQueueStmt :: Statement.Statement () ()- createQueueStmt =- preparable sql encoder decoder- where- sql = "SELECT pgmq.create('test_upgrade_queue')"- encoder = Encoders.noParams- decoder = Decoders.noResult---- | Check if last_read_at column exists in the test queue table-checkLastReadAtColumn :: Session Bool-checkLastReadAtColumn = Session.statement () checkColumnStmt- where- checkColumnStmt :: Statement.Statement () Bool- checkColumnStmt =- preparable sql encoder decoder- where- sql =- "SELECT EXISTS ( \- \ SELECT 1 FROM information_schema.columns \- \ WHERE table_schema = 'pgmq' \- \ AND table_name = 'q_test_upgrade_queue' \- \ AND column_name = 'last_read_at' \- \)"- encoder = Encoders.noParams- decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.bool))
+ vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql view
@@ -0,0 +1,291 @@+DROP FUNCTION IF EXISTS pgmq.create_non_partitioned(TEXT);+CREATE FUNCTION pgmq.create_non_partitioned(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ )+ $QUERY$,+ qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ );+ $QUERY$,+ atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);+ $QUERY$,+ qtable || '_vt_idx', qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, false, false)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );++END;+$$ LANGUAGE plpgsql;++DROP FUNCTION IF EXISTS pgmq.create_unlogged(TEXT);+CREATE FUNCTION pgmq.create_unlogged(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);++ EXECUTE FORMAT(+ $QUERY$+ CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ )+ $QUERY$,+ qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ );+ $QUERY$,+ atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);+ $QUERY$,+ qtable || '_vt_idx', qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, false, true)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION IF EXISTS pgmq.create_partitioned(TEXT, TEXT, TEXT);+CREATE FUNCTION pgmq.create_partitioned(+ queue_name TEXT,+ partition_interval TEXT DEFAULT '10000',+ retention_interval TEXT DEFAULT '100000'+)+RETURNS void AS $$+DECLARE+ partition_col TEXT;+ a_partition_col TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+ fq_qtable TEXT := 'pgmq.' || qtable;+ fq_atable TEXT := 'pgmq.' || atable;+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);+ PERFORM pgmq._ensure_pg_partman_installed();+ SELECT pgmq._get_partition_col(partition_interval) INTO partition_col;++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ ) PARTITION BY RANGE (%I)+ $QUERY$,+ qtable, partition_col+ );++ -- https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md+ -- p_parent_table - the existing parent table. MUST be schema qualified, even if in public schema.+ EXECUTE FORMAT(+ $QUERY$+ SELECT %I.create_parent(+ p_parent_table := %L,+ p_control := %L,+ p_interval := %L,+ p_type := case+ when pgmq._get_pg_partman_major_version() = 5 then 'range'+ else 'native'+ end+ )+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ fq_qtable,+ partition_col,+ partition_interval+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (%I);+ $QUERY$,+ qtable || '_part_idx', qtable, partition_col+ );++ EXECUTE FORMAT(+ $QUERY$+ UPDATE %I.part_config+ SET+ retention = %L,+ retention_keep_table = false,+ retention_keep_index = true,+ automatic_maintenance = 'on'+ WHERE parent_table = %L;+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ retention_interval,+ 'pgmq.' || qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, true, false)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );++ IF partition_col = 'enqueued_at' THEN+ a_partition_col := 'archived_at';+ ELSE+ a_partition_col := partition_col;+ END IF;++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT NOT NULL,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ ) PARTITION BY RANGE (%I);+ $QUERY$,+ atable, a_partition_col+ );++ -- https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md+ -- p_parent_table - the existing parent table. MUST be schema qualified, even if in public schema.+ EXECUTE FORMAT(+ $QUERY$+ SELECT %I.create_parent(+ p_parent_table := %L,+ p_control := %L,+ p_interval := %L,+ p_type := case+ when pgmq._get_pg_partman_major_version() = 5 then 'range'+ else 'native'+ end+ )+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ fq_atable,+ a_partition_col,+ partition_interval+ );++ EXECUTE FORMAT(+ $QUERY$+ UPDATE %I.part_config+ SET+ retention = %L,+ retention_keep_table = false,+ retention_keep_index = true,+ automatic_maintenance = 'on'+ WHERE parent_table = %L;+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ retention_interval,+ 'pgmq.' || atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++END;+$$ LANGUAGE plpgsql;
+ vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql view
@@ -0,0 +1,791 @@+CREATE TABLE IF NOT EXISTS pgmq.topic_bindings+(+ pattern text NOT NULL, -- Wildcard pattern for routing key matching (* = one segment, # = zero or more segments)+ queue_name text NOT NULL -- Name of the queue that receives messages when pattern matches+ CONSTRAINT topic_bindings_meta_queue_name_fk+ REFERENCES pgmq.meta (queue_name)+ ON DELETE CASCADE,+ bound_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, -- Timestamp when the binding was created+ compiled_regex text GENERATED ALWAYS AS (+ -- Pre-compile the pattern to regex for faster matching+ -- This avoids runtime compilation on every send_topic call+ '^' ||+ replace(+ replace(+ regexp_replace(pattern, '([.+?{}()|\[\]\\^$])', '\\\1', 'g'),+ '*', '[^.]+'+ ),+ '#', '.*'+ ) || '$'+ ) STORED, -- Computed column: stores the compiled regex pattern+ CONSTRAINT topic_bindings_unique_pattern_queue UNIQUE (pattern, queue_name)+);++-- Create covering index for better performance when scanning patterns+-- Includes queue_name and compiled_regex to allow index-only scans (no table access needed)+CREATE INDEX IF NOT EXISTS idx_topic_bindings_covering ON pgmq.topic_bindings (pattern) INCLUDE (queue_name, compiled_regex);++CREATE OR REPLACE FUNCTION pgmq.validate_routing_key(routing_key text)+ RETURNS boolean+ LANGUAGE plpgsql+ IMMUTABLE+AS+$$+BEGIN+ -- Valid routing key examples:+ -- "logs.error"+ -- "app.user-service.auth"+ -- "system_events.db.connection_failed"+ --+ -- Invalid routing key examples:+ -- "" - empty+ -- ".logs.error" - starts with dot+ -- "logs.error." - ends with dot+ -- "logs..error" - consecutive dots+ -- "logs.error!" - invalid character+ -- "logs error" - space not allowed+ -- "logs.*" - wildcards not allowed in routing keys++ IF routing_key IS NULL OR routing_key = '' THEN+ RAISE EXCEPTION 'routing_key cannot be NULL or empty';+ END IF;++ IF length(routing_key) > 255 THEN+ RAISE EXCEPTION 'routing_key length cannot exceed 255 characters, got % characters', length(routing_key);+ END IF;++ IF routing_key !~ '^[a-zA-Z0-9._-]+$' THEN+ RAISE EXCEPTION 'routing_key contains invalid characters. Only alphanumeric, dots, hyphens, and underscores are allowed. Got: %', routing_key;+ END IF;++ IF routing_key ~ '^\.' THEN+ RAISE EXCEPTION 'routing_key cannot start with a dot. Got: %', routing_key;+ END IF;++ IF routing_key ~ '\.$' THEN+ RAISE EXCEPTION 'routing_key cannot end with a dot. Got: %', routing_key;+ END IF;++ IF routing_key ~ '\.\.' THEN+ RAISE EXCEPTION 'routing_key cannot contain consecutive dots. Got: %', routing_key;+ END IF;++ RETURN true;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.validate_topic_pattern(pattern text)+ RETURNS boolean+ LANGUAGE plpgsql+ IMMUTABLE+AS+$$+BEGIN+ -- Valid pattern examples:+ -- "logs.*" - matches one segment after logs. (e.g., logs.error, logs.info)+ -- "logs.#" - matches one or more segments after logs. (e.g., logs.error, logs.api.error)+ -- "*.error" - matches one segment before .error (e.g., app.error, db.error)+ -- "#.error" - matches one or more segments before .error (e.g., app.error, x.y.error)+ -- "app.*.#" - mixed wildcards (one segment then one or more)+ -- "#" - catch-all pattern, matches any routing key+ --+ -- Invalid pattern examples:+ -- ".logs.*" - starts with dot+ -- "logs.*." - ends with dot+ -- "logs..error" - consecutive dots+ -- "logs.**" - consecutive stars+ -- "logs.##" - consecutive hashes+ -- "logs.*#" - adjacent wildcards+ -- "logs.error!" - invalid character++ IF pattern IS NULL OR pattern = '' THEN+ RAISE EXCEPTION 'pattern cannot be NULL or empty';+ END IF;++ IF length(pattern) > 255 THEN+ RAISE EXCEPTION 'pattern length cannot exceed 255 characters, got % characters', length(pattern);+ END IF;++ IF pattern !~ '^[a-zA-Z0-9._\-*#]+$' THEN+ RAISE EXCEPTION 'pattern contains invalid characters. Only alphanumeric, dots, hyphens, underscores, *, and # are allowed. Got: %', pattern;+ END IF;++ IF pattern ~ '^\.' THEN+ RAISE EXCEPTION 'pattern cannot start with a dot. Got: %', pattern;+ END IF;++ IF pattern ~ '\.$' THEN+ RAISE EXCEPTION 'pattern cannot end with a dot. Got: %', pattern;+ END IF;++ IF pattern ~ '\.\.' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive dots. Got: %', pattern;+ END IF;++ IF pattern ~ '\*\*' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive stars (**). Use # for multi-segment matching. Got: %', pattern;+ END IF;++ IF pattern ~ '##' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive hashes (##). A single # already matches zero or more segments. Got: %', pattern;+ END IF;++ IF pattern ~ '\*#' OR pattern ~ '#\*' THEN+ RAISE EXCEPTION 'pattern cannot contain adjacent wildcards (*# or #*). Separate wildcards with dots. Got: %', pattern;+ END IF;++ RETURN true;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.bind_topic(pattern text, queue_name text)+ RETURNS void+ LANGUAGE plpgsql+AS+$$+BEGIN+ PERFORM pgmq.validate_topic_pattern(pattern);+ IF queue_name IS NULL OR queue_name = '' THEN+ RAISE EXCEPTION 'queue_name cannot be NULL or empty';+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = bind_topic.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;+ END IF;++ INSERT INTO pgmq.topic_bindings (pattern, queue_name)+ VALUES (pattern, queue_name)+ ON CONFLICT ON CONSTRAINT topic_bindings_unique_pattern_queue DO NOTHING;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.unbind_topic(pattern text, queue_name text)+ RETURNS boolean+ LANGUAGE plpgsql+AS+$$+DECLARE+ rows_deleted integer;+BEGIN+ IF pattern IS NULL OR pattern = '' THEN+ RAISE EXCEPTION 'pattern cannot be NULL or empty';+ END IF;++ IF queue_name IS NULL OR queue_name = '' THEN+ RAISE EXCEPTION 'queue_name cannot be NULL or empty';+ END IF;++ DELETE+ FROM pgmq.topic_bindings+ WHERE topic_bindings.pattern = unbind_topic.pattern+ AND topic_bindings.queue_name = unbind_topic.queue_name;++ GET DIAGNOSTICS rows_deleted = ROW_COUNT;++ IF rows_deleted > 0 THEN+ RETURN true;+ ELSE+ RETURN false;+ END IF;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.test_routing(routing_key text)+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ compiled_regex text+ )+ LANGUAGE plpgsql+ STABLE+AS+$$+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);+ RETURN QUERY+ SELECT b.pattern,+ b.queue_name,+ b.compiled_regex+ FROM pgmq.topic_bindings b+ WHERE routing_key ~ b.compiled_regex+ ORDER BY b.pattern;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, headers jsonb, delay integer)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+DECLARE+ b RECORD;+ matched_count integer := 0;+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);++ IF msg IS NULL THEN+ RAISE EXCEPTION 'msg cannot be NULL';+ END IF;++ IF delay < 0 THEN+ RAISE EXCEPTION 'delay cannot be negative, got: %', delay;+ END IF;++ -- Filter matching patterns in SQL for better performance (uses index)+ -- Any failure will rollback the entire transaction+ FOR b IN+ SELECT DISTINCT tb.queue_name+ FROM pgmq.topic_bindings tb+ WHERE routing_key ~ tb.compiled_regex+ ORDER BY tb.queue_name -- Deterministic ordering, deduplicated by queue_name+ LOOP+ PERFORM pgmq.send(b.queue_name, msg, headers, delay);+ matched_count := matched_count + 1;+ END LOOP;++ RETURN matched_count;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+BEGIN+ RETURN pgmq.send_topic(routing_key, msg, NULL, 0);+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, delay integer)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+BEGIN+ RETURN pgmq.send_topic(routing_key, msg, NULL, delay);+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings()+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ bound_at TIMESTAMP WITH TIME ZONE,+ compiled_regex text+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT pattern, queue_name, bound_at, compiled_regex+ FROM pgmq.topic_bindings+ ORDER BY bound_at DESC, pattern, queue_name;+$$;++CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings(queue_name text)+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ bound_at TIMESTAMP WITH TIME ZONE,+ compiled_regex text+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT pattern, tb.queue_name, bound_at, compiled_regex+ FROM pgmq.topic_bindings tb+ WHERE tb.queue_name = list_topic_bindings.queue_name+ ORDER BY bound_at DESC, pattern;+$$;++CREATE OR REPLACE FUNCTION pgmq.list_notify_insert_throttles()+ RETURNS TABLE+ (+ queue_name text,+ throttle_interval_ms integer,+ last_notified_at TIMESTAMP WITH TIME ZONE+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT queue_name, throttle_interval_ms, last_notified_at+ FROM pgmq.notify_insert_throttle+ ORDER BY queue_name;+$$;++CREATE OR REPLACE FUNCTION pgmq.update_notify_insert(queue_name text, throttle_interval_ms integer)+ RETURNS void+ LANGUAGE plpgsql+AS+$$+BEGIN+ IF throttle_interval_ms < 0 THEN+ RAISE EXCEPTION 'throttle_interval_ms must be non-negative, got: %', throttle_interval_ms;+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = update_notify_insert.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.notify_insert_throttle WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not have notify_insert enabled. Enable it first using pgmq.enable_notify_insert()', queue_name;+ END IF;++ UPDATE pgmq.notify_insert_throttle+ SET throttle_interval_ms = update_notify_insert.throttle_interval_ms,+ last_notified_at = to_timestamp(0)+ WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name;+END;+$$;++-- send_batch_topic: Base implementation with TIMESTAMP WITH TIME ZONE delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[],+ delay TIMESTAMP WITH TIME ZONE+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE plpgsql+ VOLATILE+AS+$$+DECLARE+ b RECORD;+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);++ -- Validate batch parameters once (not per queue)+ PERFORM pgmq._validate_batch_params(msgs, headers);++ -- Filter matching patterns in SQL for better performance (uses index)+ -- Any failure will rollback the entire transaction+ FOR b IN+ SELECT DISTINCT tb.queue_name+ FROM pgmq.topic_bindings tb+ WHERE routing_key ~ tb.compiled_regex+ ORDER BY tb.queue_name -- Deterministic ordering, deduplicated by queue_name+ LOOP+ -- Use private _send_batch to avoid redundant validation+ RETURN QUERY+ SELECT b.queue_name, batch_result.msg_id+ FROM pgmq._send_batch(b.queue_name, msgs, headers, delay) AS batch_result(msg_id);+ END LOOP;++ RETURN;+END;+$$;++-- send_batch_topic: 2 args (routing_key, msgs)+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[]+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp());+$$;++-- send_batch_topic: 3 args with headers+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[]+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp());+$$;++-- send_batch_topic: 3 args with integer delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ delay integer+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp() + make_interval(secs => delay));+$$;++-- send_batch_topic: 3 args with timestamp delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ delay TIMESTAMP WITH TIME ZONE+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, delay);+$$;++-- send_batch_topic: 4 args with integer delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[],+ delay integer+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp() + make_interval(secs => delay));+$$;++-- Fix: Add validation to send_batch to ensure headers array length matches msgs array length+-- Refactored to use private functions for better performance and code reuse++-- _validate_batch_params: Private function to validate batch parameters+CREATE OR REPLACE FUNCTION pgmq._validate_batch_params(+ msgs JSONB[],+ headers JSONB[]+) RETURNS void AS $$+BEGIN+ -- Validate that msgs is not NULL or empty+ IF msgs IS NULL OR array_length(msgs, 1) IS NULL THEN+ RAISE EXCEPTION 'msgs cannot be NULL or empty';+ END IF;++ -- Validate that headers array length matches msgs array length if headers is provided+ -- Note: array_length returns NULL for empty arrays, so we use COALESCE to treat empty arrays as length 0+ IF headers IS NOT NULL AND COALESCE(array_length(headers, 1), 0) != COALESCE(array_length(msgs, 1), 0) THEN+ RAISE EXCEPTION 'headers array length (%) must match msgs array length (%)',+ COALESCE(array_length(headers, 1), 0), COALESCE(array_length(msgs, 1), 0);+ END IF;+END;+$$ LANGUAGE plpgsql;++-- _send_batch: Private function that performs the actual batch insert without validation+CREATE OR REPLACE FUNCTION pgmq._send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[],+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ INSERT INTO pgmq.%I (vt, message, headers)+ SELECT $2, unnest($1), unnest(coalesce($3, ARRAY[]::jsonb[]))+ RETURNING msg_id;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING msgs, delay, headers;+END;+$$ LANGUAGE plpgsql;++-- send_batch: Public function with validation+CREATE OR REPLACE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[],+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+BEGIN+ PERFORM pgmq._validate_batch_params(msgs, headers);+ RETURN QUERY SELECT * FROM pgmq._send_batch(queue_name, msgs, headers, delay);+END;+$$ LANGUAGE plpgsql;+++DROP FUNCTION pgmq.read_grouped(TEXT, INTEGER, INTEGER);+CREATE OR REPLACE FUNCTION pgmq.read_grouped(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH fifo_groups AS (+ -- Find the minimum msg_id for each FIFO group that's ready to be processed+ SELECT+ COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,+ MIN(msg_id) as min_msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp()+ GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')+ ),+ locked_groups AS (+ -- Lock the first available message in each FIFO group+ SELECT+ m.msg_id,+ fg.fifo_key+ FROM pgmq.%I m+ INNER JOIN fifo_groups fg ON+ COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key+ AND m.msg_id = fg.min_msg_id+ WHERE m.vt <= clock_timestamp()+ ORDER BY m.msg_id ASC+ FOR UPDATE SKIP LOCKED+ ),+ group_priorities AS (+ -- Assign priority to groups based on their oldest message+ SELECT+ fifo_key,+ msg_id as min_msg_id,+ ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority+ FROM locked_groups+ ),+ filtered_groups as (+ SELECT * FROM group_priorities gp+ WHERE NOT EXISTS (+ -- Ensure no earlier message in this group is currently being processed+ SELECT 1+ FROM pgmq.%I m2+ WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key+ AND m2.vt > clock_timestamp()+ AND m2.msg_id < gp.min_msg_id+ )+ ),+ available_messages as (+ SELECT gp.fifo_key, t.msg_id,gp.group_priority,+ ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY t.msg_id) as msg_rank_in_group+ FROM filtered_groups gp+ CROSS JOIN LATERAL (+ SELECT *+ FROM pgmq.%I t+ WHERE COALESCE(t.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key+ AND t.vt <= clock_timestamp()+ ORDER BY msg_id+ LIMIT $1 -- tip to limit query impact, we know we need at most qty in each group+ ) t+ ORDER BY gp.group_priority+ ),+ batch_selection AS (+ -- Select messages to fill batch, prioritizing earliest group+ SELECT+ msg_id,+ ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank+ FROM available_messages+ ),+ selected_messages AS (+ -- Limit to requested quantity+ SELECT msg_id+ FROM batch_selection+ WHERE overall_rank <= $1+ ORDER BY msg_id+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1,+ last_read_at = clock_timestamp()+ FROM selected_messages sm+ WHERE m.msg_id = sm.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION pgmq.read(TEXT, INTEGER, INTEGER, JSONB);+CREATE FUNCTION pgmq.read(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ conditional JSONB DEFAULT '{}'+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp() AND CASE+ WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer+ ELSE 1+ END = 1+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ last_read_at = clock_timestamp(),+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1+ FROM cte+ WHERE m.msg_id = cte.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, conditional, qtable, make_interval(secs => vt)+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION pgmq.read_with_poll(TEXT, INTEGER, INTEGER, INTEGER, INTEGER, JSONB);+CREATE FUNCTION pgmq.read_with_poll(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ max_poll_seconds INTEGER DEFAULT 5,+ poll_interval_ms INTEGER DEFAULT 100,+ conditional JSONB DEFAULT '{}'+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ r pgmq.message_record;+ stop_at TIMESTAMP;+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);+ LOOP+ IF (SELECT clock_timestamp() >= stop_at) THEN+ RETURN;+ END IF;++ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp() AND CASE+ WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer+ ELSE 1+ END = 1+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ last_read_at = clock_timestamp(),+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1+ FROM cte+ WHERE m.msg_id = cte.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, conditional, qtable, make_interval(secs => vt)+ );++ FOR r IN+ EXECUTE sql USING qty+ LOOP+ RETURN NEXT r;+ END LOOP;+ IF FOUND THEN+ RETURN;+ ELSE+ PERFORM pg_sleep(poll_interval_ms::numeric / 1000);+ END IF;+ END LOOP;+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION pgmq.pop(TEXT, INTEGER);+CREATE FUNCTION pgmq.pop(queue_name TEXT, qty INTEGER DEFAULT 1)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ result pgmq.message_record;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp()+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ DELETE from pgmq.%I+ WHERE msg_id IN (select msg_id from cte)+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$,+ qtable, qtable+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION pgmq.set_vt(TEXT, BIGINT, TIMESTAMP WITH TIME ZONE);+CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt TIMESTAMP WITH TIME ZONE)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ result pgmq.message_record;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ UPDATE pgmq.%I+ SET vt = $1+ WHERE msg_id = $2+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING vt, msg_id;+END;+$$ LANGUAGE plpgsql;++DROP FUNCTION pgmq.set_vt(TEXT, BIGINT[], TIMESTAMP WITH TIME ZONE);+CREATE FUNCTION pgmq.set_vt(+ queue_name TEXT,+ msg_ids BIGINT[],+ vt TIMESTAMP WITH TIME ZONE+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ UPDATE pgmq.%I+ SET vt = $1+ WHERE msg_id = ANY($2)+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING vt, msg_ids;+END;+$$ LANGUAGE plpgsql;
+ vendor/pgmq/pgmq-extension/sql/pgmq.sql view
@@ -0,0 +1,2076 @@+------------------------------------------------------------+-- Schema, tables, records, privileges, indexes, etc+------------------------------------------------------------+-- When installed as an extension, we don't need to create the `pgmq` schema+-- because it is automatically created by postgres due to being declared in+-- the extension control file+DO+$$+BEGIN+ IF (SELECT NOT EXISTS( SELECT 1 FROM pg_extension WHERE extname = 'pgmq')) THEN+ CREATE SCHEMA IF NOT EXISTS pgmq;+ END IF;+END+$$;++-- Table where queues and metadata about them is stored+CREATE TABLE IF NOT EXISTS pgmq.meta (+ queue_name VARCHAR UNIQUE NOT NULL,+ is_partitioned BOOLEAN NOT NULL,+ is_unlogged BOOLEAN NOT NULL,+ created_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL+);++-- Table to track notification throttling for queues+CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.notify_insert_throttle (+ queue_name VARCHAR UNIQUE NOT NULL -- Queue name (without 'q_' prefix)+ CONSTRAINT notify_insert_throttle_meta_queue_name_fk+ REFERENCES pgmq.meta (queue_name)+ ON DELETE CASCADE,+ throttle_interval_ms INTEGER NOT NULL DEFAULT 0, -- Min milliseconds between notifications (0 = no throttling)+ last_notified_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT to_timestamp(0) -- Timestamp of last sent notification+);++CREATE INDEX IF NOT EXISTS idx_notify_throttle_active+ ON pgmq.notify_insert_throttle (queue_name, last_notified_at)+ WHERE throttle_interval_ms > 0;++CREATE TABLE IF NOT EXISTS pgmq.topic_bindings+(+ pattern text NOT NULL, -- Wildcard pattern for routing key matching (* = one segment, # = zero or more segments)+ queue_name text NOT NULL -- Name of the queue that receives messages when pattern matches+ CONSTRAINT topic_bindings_meta_queue_name_fk+ REFERENCES pgmq.meta (queue_name)+ ON DELETE CASCADE,+ bound_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL, -- Timestamp when the binding was created+ compiled_regex text GENERATED ALWAYS AS (+ -- Pre-compile the pattern to regex for faster matching+ -- This avoids runtime compilation on every send_topic call+ '^' ||+ replace(+ replace(+ regexp_replace(pattern, '([.+?{}()|\[\]\\^$])', '\\\1', 'g'),+ '*', '[^.]+'+ ),+ '#', '.*'+ ) || '$'+ ) STORED, -- Computed column: stores the compiled regex pattern+ CONSTRAINT topic_bindings_unique_pattern_queue UNIQUE (pattern, queue_name)+);++-- Create covering index for better performance when scanning patterns+-- Includes queue_name and compiled_regex to allow index-only scans (no table access needed)+CREATE INDEX IF NOT EXISTS idx_topic_bindings_covering ON pgmq.topic_bindings (pattern) INCLUDE (queue_name, compiled_regex);++-- Allow pgmq.meta to be dumped by `pg_dump` when pgmq is installed as an extension+DO+$$+BEGIN+ IF EXISTS(SELECT 1 FROM pg_extension WHERE extname = 'pgmq') THEN+ PERFORM pg_catalog.pg_extension_config_dump('pgmq.meta', '');+ PERFORM pg_catalog.pg_extension_config_dump('pgmq.notify_insert_throttle', '');+ PERFORM pg_catalog.pg_extension_config_dump('pgmq.topic_bindings', '');+ END IF;+END+$$;++-- Grant permission to pg_monitor to all tables and sequences+GRANT USAGE ON SCHEMA pgmq TO pg_monitor;+GRANT SELECT ON ALL TABLES IN SCHEMA pgmq TO pg_monitor;+GRANT SELECT ON ALL SEQUENCES IN SCHEMA pgmq TO pg_monitor;+ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON TABLES TO pg_monitor;+ALTER DEFAULT PRIVILEGES IN SCHEMA pgmq GRANT SELECT ON SEQUENCES TO pg_monitor;++-- This type has the shape of a message in a queue, and is often returned by+-- pgmq functions that return messages+CREATE TYPE pgmq.message_record AS (+ msg_id BIGINT,+ read_ct INTEGER,+ enqueued_at TIMESTAMP WITH TIME ZONE,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE,+ message JSONB,+ headers JSONB+);++CREATE TYPE pgmq.queue_record AS (+ queue_name VARCHAR,+ is_partitioned BOOLEAN,+ is_unlogged BOOLEAN,+ created_at TIMESTAMP WITH TIME ZONE+);++------------------------------------------------------------+-- Functions+------------------------------------------------------------++-- prevents race conditions during queue creation by acquiring a transaction-level advisory lock+-- uses a transaction advisory lock maintain the lock until transaction commit+-- a race condition would still exist if lock was released before commit+CREATE FUNCTION pgmq.acquire_queue_lock(queue_name TEXT)+RETURNS void AS $$+BEGIN+ PERFORM pg_advisory_xact_lock(hashtext('pgmq.queue_' || queue_name));+END;+$$ LANGUAGE plpgsql;++-- read_grouped_round_robin+-- reads messages while preserving FIFO within groups and interleaving across groups (layered round-robin)+CREATE FUNCTION pgmq.read_grouped_rr(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH fifo_groups AS (+ -- Determine the absolute head (oldest) message id per FIFO group, regardless of visibility+ SELECT+ COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') AS fifo_key,+ MIN(msg_id) AS head_msg_id+ FROM pgmq.%1$I+ GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')+ ),+ eligible_groups AS (+ -- Only groups whose head message is currently visible+ -- Acquire a transaction-level advisory lock per group to prevent concurrent selection+ SELECT+ g.fifo_key,+ g.head_msg_id,+ ROW_NUMBER() OVER (ORDER BY g.head_msg_id) AS group_priority+ FROM fifo_groups g+ JOIN pgmq.%2$I h ON h.msg_id = g.head_msg_id+ WHERE h.vt <= clock_timestamp()+ AND pg_try_advisory_xact_lock(pg_catalog.hashtextextended(g.fifo_key, 0))+ ),+ available_messages AS (+ -- All currently visible messages starting at the head for each eligible group+ SELECT+ m.msg_id,+ eg.group_priority,+ ROW_NUMBER() OVER (+ PARTITION BY eg.fifo_key+ ORDER BY m.msg_id+ ) AS msg_rank_in_group+ FROM pgmq.%3$I m+ JOIN eligible_groups eg+ ON COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = eg.fifo_key+ WHERE m.vt <= clock_timestamp()+ AND m.msg_id >= eg.head_msg_id+ ),+ ordered_messages AS (+ -- Layered round-robin: take rank 1 of all groups by group_priority, then rank 2, etc.+ -- Assign selection order before locking+ SELECT msg_id, ROW_NUMBER() OVER (ORDER BY msg_rank_in_group, group_priority) as selection_order+ FROM available_messages+ ),+ selected_messages AS (+ -- Lock the messages in the correct order, preserving selection_order+ SELECT om.msg_id, om.selection_order+ FROM ordered_messages om+ JOIN pgmq.%4$I m ON m.msg_id = om.msg_id+ WHERE om.selection_order <= $1+ ORDER BY om.selection_order+ FOR UPDATE OF m SKIP LOCKED+ ),+ updated_messages AS (+ UPDATE pgmq.%5$I m+ SET+ vt = clock_timestamp() + %6$L,+ read_ct = read_ct + 1,+ last_read_at = clock_timestamp()+ FROM selected_messages sm+ WHERE m.msg_id = sm.msg_id+ AND m.vt <= clock_timestamp() -- final guard to avoid duplicate reads under races+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers, sm.selection_order+ )+ SELECT msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers+ FROM updated_messages+ ORDER BY selection_order;+ $QUERY$,+ qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++-- read_grouped_rr_with_poll+-- reads messages using round-robin layering across groups, with polling support+CREATE FUNCTION pgmq.read_grouped_rr_with_poll(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ max_poll_seconds INTEGER DEFAULT 5,+ poll_interval_ms INTEGER DEFAULT 100+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ r pgmq.message_record;+ stop_at TIMESTAMP;+BEGIN+ stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);+ LOOP+ IF (SELECT clock_timestamp() >= stop_at) THEN+ RETURN;+ END IF;++ FOR r IN+ SELECT * FROM pgmq.read_grouped_rr(queue_name, vt, qty)+ LOOP+ RETURN NEXT r;+ END LOOP;+ IF FOUND THEN+ RETURN;+ ELSE+ PERFORM pg_sleep(poll_interval_ms::numeric / 1000);+ END IF;+ END LOOP;+END;+$$ LANGUAGE plpgsql;++-- a helper to format table names and check for invalid characters+CREATE FUNCTION pgmq.format_table_name(queue_name text, prefix text)+RETURNS TEXT AS $$+BEGIN+ IF queue_name ~ '\$|;|--|'''+ THEN+ RAISE EXCEPTION 'queue name contains invalid characters: $, ;, --, or \''';+ END IF;+ RETURN lower(prefix || '_' || queue_name);+END;+$$ LANGUAGE plpgsql;++-- read+-- reads a number of messages from a queue, setting a visibility timeout on them+CREATE FUNCTION pgmq.read(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ conditional JSONB DEFAULT '{}'+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp() AND CASE+ WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer+ ELSE 1+ END = 1+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ last_read_at = clock_timestamp(),+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1+ FROM cte+ WHERE m.msg_id = cte.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, conditional, qtable, make_interval(secs => vt)+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++-- read_grouped+-- reads messages with AWS SQS FIFO-style batch retrieval behavior+-- attempts to return as many messages as possible from the same message group+CREATE FUNCTION pgmq.read_grouped(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH fifo_groups AS (+ -- Find the minimum msg_id for each FIFO group that's ready to be processed+ SELECT+ COALESCE(headers->>'x-pgmq-group', '_default_fifo_group') as fifo_key,+ MIN(msg_id) as min_msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp()+ GROUP BY COALESCE(headers->>'x-pgmq-group', '_default_fifo_group')+ ),+ locked_groups AS (+ -- Lock the first available message in each FIFO group+ SELECT+ m.msg_id,+ fg.fifo_key+ FROM pgmq.%I m+ INNER JOIN fifo_groups fg ON+ COALESCE(m.headers->>'x-pgmq-group', '_default_fifo_group') = fg.fifo_key+ AND m.msg_id = fg.min_msg_id+ WHERE m.vt <= clock_timestamp()+ ORDER BY m.msg_id ASC+ FOR UPDATE SKIP LOCKED+ ),+ group_priorities AS (+ -- Assign priority to groups based on their oldest message+ SELECT+ fifo_key,+ msg_id as min_msg_id,+ ROW_NUMBER() OVER (ORDER BY msg_id) as group_priority+ FROM locked_groups+ ),+ filtered_groups as (+ SELECT * FROM group_priorities gp+ WHERE NOT EXISTS (+ -- Ensure no earlier message in this group is currently being processed+ SELECT 1+ FROM pgmq.%I m2+ WHERE COALESCE(m2.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key+ AND m2.vt > clock_timestamp()+ AND m2.msg_id < gp.min_msg_id+ )+ ),+ available_messages as (+ SELECT gp.fifo_key, t.msg_id,gp.group_priority,+ ROW_NUMBER() OVER (PARTITION BY gp.fifo_key ORDER BY t.msg_id) as msg_rank_in_group+ FROM filtered_groups gp+ CROSS JOIN LATERAL (+ SELECT *+ FROM pgmq.%I t+ WHERE COALESCE(t.headers->>'x-pgmq-group', '_default_fifo_group') = gp.fifo_key+ AND t.vt <= clock_timestamp()+ ORDER BY msg_id+ LIMIT $1 -- tip to limit query impact, we know we need at most qty in each group+ ) t+ ORDER BY gp.group_priority+ ),+ batch_selection AS (+ -- Select messages to fill batch, prioritizing earliest group+ SELECT+ msg_id,+ ROW_NUMBER() OVER (ORDER BY group_priority, msg_rank_in_group) as overall_rank+ FROM available_messages+ ),+ selected_messages AS (+ -- Limit to requested quantity+ SELECT msg_id+ FROM batch_selection+ WHERE overall_rank <= $1+ ORDER BY msg_id+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1,+ last_read_at = clock_timestamp()+ FROM selected_messages sm+ WHERE m.msg_id = sm.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, qtable, qtable, qtable, qtable, make_interval(secs => vt)+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++-- read_grouped_with_poll+-- reads messages with AWS SQS FIFO-style batch retrieval behavior, with polling support+CREATE FUNCTION pgmq.read_grouped_with_poll(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ max_poll_seconds INTEGER DEFAULT 5,+ poll_interval_ms INTEGER DEFAULT 100+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ r pgmq.message_record;+ stop_at TIMESTAMP;+BEGIN+ stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);+ LOOP+ IF (SELECT clock_timestamp() >= stop_at) THEN+ RETURN;+ END IF;++ FOR r IN+ SELECT * FROM pgmq.read_grouped(queue_name, vt, qty)+ LOOP+ RETURN NEXT r;+ END LOOP;+ IF FOUND THEN+ RETURN;+ ELSE+ PERFORM pg_sleep(poll_interval_ms::numeric / 1000);+ END IF;+ END LOOP;+END;+$$ LANGUAGE plpgsql;++---- read_with_poll+---- reads a number of messages from a queue, setting a visibility timeout on them+CREATE FUNCTION pgmq.read_with_poll(+ queue_name TEXT,+ vt INTEGER,+ qty INTEGER,+ max_poll_seconds INTEGER DEFAULT 5,+ poll_interval_ms INTEGER DEFAULT 100,+ conditional JSONB DEFAULT '{}'+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ r pgmq.message_record;+ stop_at TIMESTAMP;+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ stop_at := clock_timestamp() + make_interval(secs => max_poll_seconds);+ LOOP+ IF (SELECT clock_timestamp() >= stop_at) THEN+ RETURN;+ END IF;++ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp() AND CASE+ WHEN %L != '{}'::jsonb THEN (message @> %2$L)::integer+ ELSE 1+ END = 1+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ UPDATE pgmq.%I m+ SET+ last_read_at = clock_timestamp(),+ vt = clock_timestamp() + %L,+ read_ct = read_ct + 1+ FROM cte+ WHERE m.msg_id = cte.msg_id+ RETURNING m.msg_id, m.read_ct, m.enqueued_at, m.last_read_at, m.vt, m.message, m.headers;+ $QUERY$,+ qtable, conditional, qtable, make_interval(secs => vt)+ );++ FOR r IN+ EXECUTE sql USING qty+ LOOP+ RETURN NEXT r;+ END LOOP;+ IF FOUND THEN+ RETURN;+ ELSE+ PERFORM pg_sleep(poll_interval_ms::numeric / 1000);+ END IF;+ END LOOP;+END;+$$ LANGUAGE plpgsql;++---- archive+---- removes a message from the queue, and sends it to the archive, where its+---- saved permanently.+CREATE FUNCTION pgmq.archive(+ queue_name TEXT,+ msg_id BIGINT+)+RETURNS BOOLEAN AS $$+DECLARE+ sql TEXT;+ result BIGINT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH archived AS (+ DELETE FROM pgmq.%I+ WHERE msg_id = $1+ RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers+ )+ INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)+ SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers+ FROM archived+ RETURNING msg_id;+ $QUERY$,+ qtable, atable+ );+ EXECUTE sql USING msg_id INTO result;+ RETURN NOT (result IS NULL);+END;+$$ LANGUAGE plpgsql;++---- archive+---- removes an array of message ids from the queue, and sends it to the archive,+---- where these messages will be saved permanently.+CREATE FUNCTION pgmq.archive(+ queue_name TEXT,+ msg_ids BIGINT[]+)+RETURNS SETOF BIGINT AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH archived AS (+ DELETE FROM pgmq.%I+ WHERE msg_id = ANY($1)+ RETURNING msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers+ )+ INSERT INTO pgmq.%I (msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers)+ SELECT msg_id, vt, read_ct, enqueued_at, last_read_at, message, headers+ FROM archived+ RETURNING msg_id;+ $QUERY$,+ qtable, atable+ );+ RETURN QUERY EXECUTE sql USING msg_ids;+END;+$$ LANGUAGE plpgsql;++---- delete+---- deletes a message id from the queue permanently+CREATE FUNCTION pgmq.delete(+ queue_name TEXT,+ msg_id BIGINT+)+RETURNS BOOLEAN AS $$+DECLARE+ sql TEXT;+ result BIGINT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ DELETE FROM pgmq.%I+ WHERE msg_id = $1+ RETURNING msg_id+ $QUERY$,+ qtable+ );+ EXECUTE sql USING msg_id INTO result;+ RETURN NOT (result IS NULL);+END;+$$ LANGUAGE plpgsql;++---- delete+---- deletes an array of message ids from the queue permanently+CREATE FUNCTION pgmq.delete(+ queue_name TEXT,+ msg_ids BIGINT[]+)+RETURNS SETOF BIGINT AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ DELETE FROM pgmq.%I+ WHERE msg_id = ANY($1)+ RETURNING msg_id+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING msg_ids;+END;+$$ LANGUAGE plpgsql;++-- send: actual implementation+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB,+ headers JSONB,+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ INSERT INTO pgmq.%I (vt, message, headers)+ VALUES ($2, $1, $3)+ RETURNING msg_id;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING msg, delay, headers;+END;+$$ LANGUAGE plpgsql;++-- send: 2 args, no delay or headers+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp());+$$ LANGUAGE sql;++-- send: 3 args with headers+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB,+ headers JSONB+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp());+$$ LANGUAGE sql;++-- send: 3 args with integer delay+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB,+ delay INTEGER+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send(queue_name, msg, NULL, clock_timestamp() + make_interval(secs => delay));+$$ LANGUAGE sql;++-- send: 3 args with timestamp+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB,+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send(queue_name, msg, NULL, delay);+$$ LANGUAGE sql;++-- send: 4 args with integer delay+CREATE FUNCTION pgmq.send(+ queue_name TEXT,+ msg JSONB,+ headers JSONB,+ delay INTEGER+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send(queue_name, msg, headers, clock_timestamp() + make_interval(secs => delay));+$$ LANGUAGE sql;++-- _validate_batch_params: Private function to validate batch parameters+CREATE FUNCTION pgmq._validate_batch_params(+ msgs JSONB[],+ headers JSONB[]+) RETURNS void AS $$+BEGIN+ -- Validate that msgs is not NULL or empty+ IF msgs IS NULL OR array_length(msgs, 1) IS NULL THEN+ RAISE EXCEPTION 'msgs cannot be NULL or empty';+ END IF;++ -- Validate that headers array length matches msgs array length if headers is provided+ -- Note: array_length returns NULL for empty arrays, so we use COALESCE to treat empty arrays as length 0+ IF headers IS NOT NULL AND COALESCE(array_length(headers, 1), 0) != COALESCE(array_length(msgs, 1), 0) THEN+ RAISE EXCEPTION 'headers array length (%) must match msgs array length (%)',+ COALESCE(array_length(headers, 1), 0), COALESCE(array_length(msgs, 1), 0);+ END IF;+END;+$$ LANGUAGE plpgsql;++-- _send_batch: Private function that performs the actual batch insert without validation+CREATE FUNCTION pgmq._send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[],+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ INSERT INTO pgmq.%I (vt, message, headers)+ SELECT $2, unnest($1), unnest(coalesce($3, ARRAY[]::jsonb[]))+ RETURNING msg_id;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING msgs, delay, headers;+END;+$$ LANGUAGE plpgsql;++-- send_batch: Public function with validation+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[],+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+BEGIN+ PERFORM pgmq._validate_batch_params(msgs, headers);+ RETURN QUERY SELECT * FROM pgmq._send_batch(queue_name, msgs, headers, delay);+END;+$$ LANGUAGE plpgsql;++-- send batch: 2 args+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[]+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp());+$$ LANGUAGE sql;++-- send batch: 3 args with headers+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[]+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp());+$$ LANGUAGE sql;++-- send batch: 3 args with integer delay+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ delay INTEGER+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, clock_timestamp() + make_interval(secs => delay));+$$ LANGUAGE sql;++-- send batch: 3 args with timestamp+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ delay TIMESTAMP WITH TIME ZONE+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send_batch(queue_name, msgs, NULL, delay);+$$ LANGUAGE sql;++-- send_batch: 4 args with integer delay+CREATE FUNCTION pgmq.send_batch(+ queue_name TEXT,+ msgs JSONB[],+ headers JSONB[],+ delay INTEGER+) RETURNS SETOF BIGINT AS $$+ SELECT * FROM pgmq.send_batch(queue_name, msgs, headers, clock_timestamp() + make_interval(secs => delay));+$$ LANGUAGE sql;++-- returned by pgmq.metrics() and pgmq.metrics_all+CREATE TYPE pgmq.metrics_result AS (+ queue_name text,+ queue_length bigint,+ newest_msg_age_sec int,+ oldest_msg_age_sec int,+ total_messages bigint,+ scrape_time timestamp with time zone,+ queue_visible_length bigint+);++-- get metrics for a single queue+CREATE FUNCTION pgmq.metrics(queue_name TEXT)+RETURNS pgmq.metrics_result AS $$+DECLARE+ result_row pgmq.metrics_result;+ query TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ query := FORMAT(+ $QUERY$+ WITH q_summary AS (+ SELECT+ count(*) as queue_length,+ count(CASE WHEN vt <= NOW() THEN 1 END) as queue_visible_length,+ EXTRACT(epoch FROM (NOW() - max(enqueued_at)))::int as newest_msg_age_sec,+ EXTRACT(epoch FROM (NOW() - min(enqueued_at)))::int as oldest_msg_age_sec,+ NOW() as scrape_time+ FROM pgmq.%I+ ),+ all_metrics AS (+ SELECT CASE+ WHEN is_called THEN last_value ELSE 0+ END as total_messages+ FROM pgmq.%I+ )+ SELECT+ %L as queue_name,+ q_summary.queue_length,+ q_summary.newest_msg_age_sec,+ q_summary.oldest_msg_age_sec,+ all_metrics.total_messages,+ q_summary.scrape_time,+ q_summary.queue_visible_length+ FROM q_summary, all_metrics+ $QUERY$,+ qtable, qtable || '_msg_id_seq', queue_name+ );+ EXECUTE query INTO result_row;+ RETURN result_row;+END;+$$ LANGUAGE plpgsql;++-- get metrics for all queues+CREATE FUNCTION pgmq."metrics_all"()+RETURNS SETOF pgmq.metrics_result AS $$+DECLARE+ row_name RECORD;+ result_row pgmq.metrics_result;+BEGIN+ FOR row_name IN SELECT queue_name FROM pgmq.meta LOOP+ result_row := pgmq.metrics(row_name.queue_name);+ RETURN NEXT result_row;+ END LOOP;+END;+$$ LANGUAGE plpgsql;++-- list queues+CREATE FUNCTION pgmq."list_queues"()+RETURNS SETOF pgmq.queue_record AS $$+BEGIN+ RETURN QUERY SELECT * FROM pgmq.meta;+END+$$ LANGUAGE plpgsql;++-- purge queue, deleting all entries in it.+CREATE OR REPLACE FUNCTION pgmq."purge_queue"(queue_name TEXT)+RETURNS BIGINT AS $$+DECLARE+ deleted_count INTEGER;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ -- Get the row count before truncating+ EXECUTE format('SELECT count(*) FROM pgmq.%I', qtable) INTO deleted_count;++ -- Use TRUNCATE for better performance on large tables+ EXECUTE format('TRUNCATE TABLE pgmq.%I', qtable);++ -- Return the number of purged rows+ RETURN deleted_count;+END+$$ LANGUAGE plpgsql;++-- unassign archive, so it can be kept when a queue is deleted+CREATE FUNCTION pgmq."detach_archive"(queue_name TEXT)+RETURNS VOID AS $$+DECLARE+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ RAISE WARNING 'detach_archive(queue_name) is deprecated and is a no-op. It will be removed in PGMQ v2.0. Archive tables are no longer member objects.';+END+$$ LANGUAGE plpgsql;++-- pop: implementation+CREATE FUNCTION pgmq.pop(queue_name TEXT, qty INTEGER DEFAULT 1)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ result pgmq.message_record;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ WITH cte AS+ (+ SELECT msg_id+ FROM pgmq.%I+ WHERE vt <= clock_timestamp()+ ORDER BY msg_id ASC+ LIMIT $1+ FOR UPDATE SKIP LOCKED+ )+ DELETE from pgmq.%I+ WHERE msg_id IN (select msg_id from cte)+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$,+ qtable, qtable+ );+ RETURN QUERY EXECUTE sql USING qty;+END;+$$ LANGUAGE plpgsql;++-- Sets timestamp vt of a message, returns it+CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt TIMESTAMP WITH TIME ZONE)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ result pgmq.message_record;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ UPDATE pgmq.%I+ SET vt = $1+ WHERE msg_id = $2+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$, + qtable+ );+ RETURN QUERY EXECUTE sql USING vt, msg_id;+END;+$$ LANGUAGE plpgsql;++-- Sets integer vt of a message, returns it+CREATE FUNCTION pgmq.set_vt(queue_name TEXT, msg_id BIGINT, vt INTEGER)+RETURNS SETOF pgmq.message_record AS $$+ SELECT * FROM pgmq.set_vt(queue_name, msg_id, clock_timestamp() + make_interval(secs => vt));+$$ LANGUAGE sql;++-- Sets timestamp vt of multiple messages, returns them+CREATE FUNCTION pgmq.set_vt(+ queue_name TEXT,+ msg_ids BIGINT[],+ vt TIMESTAMP WITH TIME ZONE+)+RETURNS SETOF pgmq.message_record AS $$+DECLARE+ sql TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+BEGIN+ sql := FORMAT(+ $QUERY$+ UPDATE pgmq.%I+ SET vt = $1+ WHERE msg_id = ANY($2)+ RETURNING msg_id, read_ct, enqueued_at, last_read_at, vt, message, headers;+ $QUERY$,+ qtable+ );+ RETURN QUERY EXECUTE sql USING vt, msg_ids;+END;+$$ LANGUAGE plpgsql;++-- Sets integer vt of multiple messages, returns them+CREATE FUNCTION pgmq.set_vt(+ queue_name TEXT,+ msg_ids BIGINT[],+ vt INTEGER+)+RETURNS SETOF pgmq.message_record AS $$+ SELECT * FROM pgmq.set_vt(queue_name, msg_ids, clock_timestamp() + make_interval(secs => vt));+$$ LANGUAGE sql;++CREATE FUNCTION pgmq._get_pg_partman_schema()+RETURNS TEXT AS $$+ SELECT+ extnamespace::regnamespace::text+ FROM+ pg_extension+ WHERE+ extname = 'pg_partman';+$$ LANGUAGE SQL;++CREATE FUNCTION pgmq.drop_queue(queue_name TEXT, partitioned BOOLEAN)+RETURNS BOOLEAN AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ fq_qtable TEXT := 'pgmq.' || qtable;+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+ fq_atable TEXT := 'pgmq.' || atable;+BEGIN+ RAISE WARNING 'drop_queue(queue_name, partitioned) is deprecated and will be removed in PGMQ v2.0. Use drop_queue(queue_name) instead';++ PERFORM pgmq.drop_queue(queue_name);++ RETURN TRUE;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq.drop_queue(queue_name TEXT)+RETURNS BOOLEAN AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ fq_qtable TEXT := 'pgmq.' || qtable;+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+ fq_atable TEXT := 'pgmq.' || atable;+ partitioned BOOLEAN;+BEGIN+ PERFORM pgmq.acquire_queue_lock(queue_name);+ EXECUTE FORMAT(+ $QUERY$+ SELECT is_partitioned FROM pgmq.meta WHERE queue_name = %L+ $QUERY$,+ queue_name+ ) INTO partitioned;++ -- check if the queue exists+ IF NOT EXISTS (+ SELECT 1+ FROM information_schema.tables+ WHERE table_name = qtable and table_schema = 'pgmq'+ ) THEN+ RAISE NOTICE 'pgmq queue `%` does not exist', queue_name;+ RETURN FALSE;+ END IF;++ EXECUTE FORMAT(+ $QUERY$+ DROP TABLE IF EXISTS pgmq.%I+ $QUERY$,+ qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ DROP TABLE IF EXISTS pgmq.%I+ $QUERY$,+ atable+ );++ IF EXISTS (+ SELECT 1+ FROM information_schema.tables+ WHERE table_name = 'meta' and table_schema = 'pgmq'+ ) THEN+ EXECUTE FORMAT(+ $QUERY$+ DELETE FROM pgmq.meta WHERE queue_name = %L+ $QUERY$,+ queue_name+ );+ END IF;++ IF partitioned THEN+ EXECUTE FORMAT(+ $QUERY$+ DELETE FROM %I.part_config where parent_table in (%L, %L)+ $QUERY$,+ pgmq._get_pg_partman_schema(), fq_qtable, fq_atable+ );+ END IF;++ RETURN TRUE;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq.validate_queue_name(queue_name TEXT)+RETURNS void AS $$+BEGIN+ IF length(queue_name) > 47 THEN+ -- complete table identifier must be <= 63+ -- https://www.postgresql.org/docs/17/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS+ -- e.g. template_pgmq_q_my_queue is an identifier for my_queue when partitioned+ -- template_pgmq_q_ (16) + <a max length queue name> (47) = 63 + RAISE EXCEPTION 'queue name is too long, maximum length is 47 characters';+ END IF;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq._belongs_to_pgmq(table_name TEXT)+RETURNS BOOLEAN AS $$+DECLARE+ sql TEXT;+ result BOOLEAN;+BEGIN+ SELECT EXISTS (+ SELECT 1+ FROM pg_depend+ WHERE refobjid = (SELECT oid FROM pg_extension WHERE extname = 'pgmq')+ AND objid = (+ SELECT oid+ FROM pg_class+ WHERE relname = table_name+ )+ ) INTO result;+ RETURN result;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq.create_non_partitioned(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ )+ $QUERY$,+ qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ );+ $QUERY$,+ atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);+ $QUERY$,+ qtable || '_vt_idx', qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, false, false)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );++END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq.create_unlogged(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);++ EXECUTE FORMAT(+ $QUERY$+ CREATE UNLOGGED TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ )+ $QUERY$,+ qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT PRIMARY KEY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ );+ $QUERY$,+ atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (vt ASC);+ $QUERY$,+ qtable || '_vt_idx', qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, false, true)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq._get_partition_col(partition_interval TEXT)+RETURNS TEXT AS $$+DECLARE+ num INTEGER;+BEGIN+ BEGIN+ num := partition_interval::INTEGER;+ RETURN 'msg_id';+ EXCEPTION+ WHEN others THEN+ RETURN 'enqueued_at';+ END;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq._extension_exists(extension_name TEXT)+ RETURNS BOOLEAN+ LANGUAGE SQL+AS $$+SELECT EXISTS (+ SELECT 1+ FROM pg_extension+ WHERE extname = extension_name+)+$$;++CREATE FUNCTION pgmq._ensure_pg_partman_installed()+RETURNS void AS $$+BEGIN+ IF NOT pgmq._extension_exists('pg_partman') THEN+ RAISE EXCEPTION 'pg_partman is required for partitioned queues';+ END IF;+END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq._get_pg_partman_major_version()+RETURNS INT+LANGUAGE SQL+AS $$+ SELECT split_part(extversion, '.', 1)::INT+ FROM pg_extension+ WHERE extname = 'pg_partman'+$$;++CREATE FUNCTION pgmq.create_partitioned(+ queue_name TEXT,+ partition_interval TEXT DEFAULT '10000',+ retention_interval TEXT DEFAULT '100000'+)+RETURNS void AS $$+DECLARE+ partition_col TEXT;+ a_partition_col TEXT;+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ qtable_seq TEXT := qtable || '_msg_id_seq';+ atable TEXT := pgmq.format_table_name(queue_name, 'a');+ fq_qtable TEXT := 'pgmq.' || qtable;+ fq_atable TEXT := 'pgmq.' || atable;+BEGIN+ PERFORM pgmq.validate_queue_name(queue_name);+ PERFORM pgmq.acquire_queue_lock(queue_name);+ PERFORM pgmq._ensure_pg_partman_installed();+ SELECT pgmq._get_partition_col(partition_interval) INTO partition_col;++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT GENERATED ALWAYS AS IDENTITY,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ ) PARTITION BY RANGE (%I)+ $QUERY$,+ qtable, partition_col+ );++ -- https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md+ -- p_parent_table - the existing parent table. MUST be schema qualified, even if in public schema.+ EXECUTE FORMAT(+ $QUERY$+ SELECT %I.create_parent(+ p_parent_table := %L,+ p_control := %L,+ p_interval := %L,+ p_type := case+ when pgmq._get_pg_partman_major_version() = 5 then 'range'+ else 'native'+ end+ )+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ fq_qtable,+ partition_col,+ partition_interval+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (%I);+ $QUERY$,+ qtable || '_part_idx', qtable, partition_col+ );++ EXECUTE FORMAT(+ $QUERY$+ UPDATE %I.part_config+ SET+ retention = %L,+ retention_keep_table = false,+ retention_keep_index = true,+ automatic_maintenance = 'on'+ WHERE parent_table = %L;+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ retention_interval,+ 'pgmq.' || qtable+ );++ EXECUTE FORMAT(+ $QUERY$+ INSERT INTO pgmq.meta (queue_name, is_partitioned, is_unlogged)+ VALUES (%L, true, false)+ ON CONFLICT+ DO NOTHING;+ $QUERY$,+ queue_name+ );++ IF partition_col = 'enqueued_at' THEN+ a_partition_col := 'archived_at';+ ELSE+ a_partition_col := partition_col;+ END IF;++ EXECUTE FORMAT(+ $QUERY$+ CREATE TABLE IF NOT EXISTS pgmq.%I (+ msg_id BIGINT NOT NULL,+ read_ct INT DEFAULT 0 NOT NULL,+ enqueued_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ last_read_at TIMESTAMP WITH TIME ZONE,+ archived_at TIMESTAMP WITH TIME ZONE DEFAULT now() NOT NULL,+ vt TIMESTAMP WITH TIME ZONE NOT NULL,+ message JSONB,+ headers JSONB+ ) PARTITION BY RANGE (%I);+ $QUERY$,+ atable, a_partition_col+ );++ -- https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md+ -- p_parent_table - the existing parent table. MUST be schema qualified, even if in public schema.+ EXECUTE FORMAT(+ $QUERY$+ SELECT %I.create_parent(+ p_parent_table := %L,+ p_control := %L,+ p_interval := %L,+ p_type := case+ when pgmq._get_pg_partman_major_version() = 5 then 'range'+ else 'native'+ end+ )+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ fq_atable,+ a_partition_col,+ partition_interval+ );++ EXECUTE FORMAT(+ $QUERY$+ UPDATE %I.part_config+ SET+ retention = %L,+ retention_keep_table = false,+ retention_keep_index = true,+ automatic_maintenance = 'on'+ WHERE parent_table = %L;+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ retention_interval,+ 'pgmq.' || atable+ );++ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I (archived_at);+ $QUERY$,+ 'archived_at_idx_' || queue_name, atable+ );++END;+$$ LANGUAGE plpgsql;++CREATE FUNCTION pgmq.create(queue_name TEXT)+RETURNS void AS $$+BEGIN+ PERFORM pgmq.create_non_partitioned(queue_name);+END;+$$ LANGUAGE plpgsql;++-- _create_fifo_index_if_not_exists+-- internal function to create GIN index on headers for better FIFO performance+CREATE OR REPLACE FUNCTION pgmq._create_fifo_index_if_not_exists(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ index_name TEXT := qtable || '_fifo_idx';+BEGIN+ -- Create GIN index on headers for efficient FIFO key lookups+ EXECUTE FORMAT(+ $QUERY$+ CREATE INDEX IF NOT EXISTS %I ON pgmq.%I USING GIN (headers);+ $QUERY$,+ index_name, qtable+ );+END;+$$ LANGUAGE plpgsql;++-- create_fifo_index+-- creates a GIN index on the headers column to improve FIFO read performance+CREATE FUNCTION pgmq.create_fifo_index(queue_name TEXT)+RETURNS void AS $$+BEGIN+ PERFORM pgmq._create_fifo_index_if_not_exists(queue_name);+END;+$$ LANGUAGE plpgsql;++-- create_fifo_indexes_all+-- creates FIFO indexes on all existing queues+CREATE FUNCTION pgmq.create_fifo_indexes_all()+RETURNS void AS $$+DECLARE+ queue_record RECORD;+BEGIN+ FOR queue_record IN SELECT queue_name FROM pgmq.meta LOOP+ PERFORM pgmq.create_fifo_index(queue_record.queue_name);+ END LOOP;+END;+$$ LANGUAGE plpgsql;++CREATE OR REPLACE FUNCTION pgmq.convert_archive_partitioned(+ table_name TEXT,+ partition_interval TEXT DEFAULT '10000',+ retention_interval TEXT DEFAULT '100000',+ leading_partition INT DEFAULT 10+)+RETURNS void AS $$+DECLARE+ a_table_name TEXT := pgmq.format_table_name(table_name, 'a');+ a_table_name_old TEXT := pgmq.format_table_name(table_name, 'a') || '_old';+ qualified_a_table_name TEXT := format('pgmq.%I', a_table_name);+ partition_col TEXT;+ a_partition_col TEXT;+BEGIN++ PERFORM c.relkind+ FROM pg_class c+ JOIN pg_namespace n ON n.oid = c.relnamespace+ WHERE c.relname = a_table_name+ AND c.relkind = 'p';++ IF FOUND THEN+ RAISE NOTICE 'Table %s is already partitioned', a_table_name;+ RETURN;+ END IF;++ PERFORM c.relkind+ FROM pg_class c+ JOIN pg_namespace n ON n.oid = c.relnamespace+ WHERE c.relname = a_table_name+ AND c.relkind = 'r';++ IF NOT FOUND THEN+ RAISE NOTICE 'Table %s does not exists', a_table_name;+ RETURN;+ END IF;++ SELECT pgmq._get_partition_col(partition_interval) INTO partition_col;++ -- For archive tables, use archived_at for time-based partitioning+ IF partition_col = 'enqueued_at' THEN+ a_partition_col := 'archived_at';+ ELSE+ a_partition_col := partition_col;+ END IF;++ EXECUTE 'ALTER TABLE ' || qualified_a_table_name || ' RENAME TO ' || a_table_name_old;++ -- When partitioning by time (archived_at), we need to exclude constraints and indexes+ -- because the existing PRIMARY KEY on msg_id alone is incompatible with partitioning by archived_at.+ -- When partitioning by msg_id, we can keep all constraints including PRIMARY KEY.+ IF a_partition_col = 'archived_at' THEN+ EXECUTE format( 'CREATE TABLE pgmq.%I (LIKE pgmq.%I including defaults including generated including storage including comments) PARTITION BY RANGE (%I)', a_table_name, a_table_name_old, a_partition_col );+ ELSE+ EXECUTE format( 'CREATE TABLE pgmq.%I (LIKE pgmq.%I including all) PARTITION BY RANGE (%I)', a_table_name, a_table_name_old, a_partition_col );+ END IF;++ EXECUTE 'ALTER INDEX pgmq.archived_at_idx_' || table_name || ' RENAME TO archived_at_idx_' || table_name || '_old';+ EXECUTE 'CREATE INDEX archived_at_idx_'|| table_name || ' ON ' || qualified_a_table_name ||'(archived_at)';++ -- https://github.com/pgpartman/pg_partman/blob/master/doc/pg_partman.md+ -- p_parent_table - the existing parent table. MUST be schema qualified, even if in public schema.+ EXECUTE FORMAT(+ $QUERY$+ SELECT %I.create_parent(+ p_parent_table := %L,+ p_control := %L,+ p_interval := %L,+ p_type := case+ when pgmq._get_pg_partman_major_version() = 5 then 'range'+ else 'native'+ end+ )+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ qualified_a_table_name,+ a_partition_col,+ partition_interval+ );++ EXECUTE FORMAT(+ $QUERY$+ UPDATE %I.part_config+ SET+ retention = %L,+ retention_keep_table = false,+ retention_keep_index = false,+ infinite_time_partitions = true+ WHERE+ parent_table = %L;+ $QUERY$,+ pgmq._get_pg_partman_schema(),+ retention_interval,+ qualified_a_table_name+ );+END;+$$ LANGUAGE plpgsql;++CREATE OR REPLACE FUNCTION pgmq.notify_queue_listeners()+RETURNS TRIGGER AS $$+DECLARE+ queue_name_extracted TEXT; -- Queue name extracted from trigger table name+ updated_count INTEGER; -- Number of rows updated (0 or 1)+BEGIN+ queue_name_extracted := substring(TG_TABLE_NAME from 3);++ UPDATE pgmq.notify_insert_throttle+ SET last_notified_at = clock_timestamp()+ WHERE queue_name = queue_name_extracted+ AND (+ throttle_interval_ms = 0 -- No throttling configured+ OR clock_timestamp() - last_notified_at >=+ (throttle_interval_ms * INTERVAL '1 millisecond') -- Throttle interval has elapsed+ );++ -- Check how many rows were updated (will be 0 or 1)+ GET DIAGNOSTICS updated_count = ROW_COUNT;++ IF updated_count > 0 THEN+ PERFORM PG_NOTIFY('pgmq.' || TG_TABLE_NAME || '.' || TG_OP, NULL);+ END IF;++RETURN NEW;+END;+$$ LANGUAGE plpgsql;++CREATE OR REPLACE FUNCTION pgmq.enable_notify_insert(queue_name TEXT, throttle_interval_ms INTEGER DEFAULT 250)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ v_queue_name TEXT := queue_name;+ v_throttle_interval_ms INTEGER := throttle_interval_ms;+BEGIN+ -- Validate that throttle_interval_ms is non-negative+ IF v_throttle_interval_ms < 0 THEN+ RAISE EXCEPTION 'throttle_interval_ms must be non-negative';+ END IF;++ -- Validate that the queue table exists+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'pgmq' AND table_name = qtable) THEN+ RAISE EXCEPTION 'Queue "%" does not exist. Create it first using pgmq.create()', v_queue_name;+ END IF;++ PERFORM pgmq.disable_notify_insert(v_queue_name);++ INSERT INTO pgmq.notify_insert_throttle (queue_name, throttle_interval_ms)+ VALUES (v_queue_name, v_throttle_interval_ms)+ ON CONFLICT ON CONSTRAINT notify_insert_throttle_queue_name_key DO UPDATE+ SET throttle_interval_ms = EXCLUDED.throttle_interval_ms,+ last_notified_at = to_timestamp(0);++ EXECUTE FORMAT(+ $QUERY$+ CREATE CONSTRAINT TRIGGER trigger_notify_queue_insert_listeners+ AFTER INSERT ON pgmq.%I+ DEFERRABLE FOR EACH ROW+ EXECUTE PROCEDURE pgmq.notify_queue_listeners()+ $QUERY$,+ qtable+ );+END;+$$ LANGUAGE plpgsql;++CREATE OR REPLACE FUNCTION pgmq.disable_notify_insert(queue_name TEXT)+RETURNS void AS $$+DECLARE+ qtable TEXT := pgmq.format_table_name(queue_name, 'q');+ v_queue_name TEXT := queue_name;+BEGIN+ EXECUTE FORMAT(+ $QUERY$+ DROP TRIGGER IF EXISTS trigger_notify_queue_insert_listeners ON pgmq.%I;+ $QUERY$,+ qtable+ );++ DELETE FROM pgmq.notify_insert_throttle nit WHERE nit.queue_name = v_queue_name;+END;+$$ LANGUAGE plpgsql;++CREATE OR REPLACE FUNCTION pgmq.list_notify_insert_throttles()+ RETURNS TABLE+ (+ queue_name text,+ throttle_interval_ms integer,+ last_notified_at TIMESTAMP WITH TIME ZONE+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT queue_name, throttle_interval_ms, last_notified_at+ FROM pgmq.notify_insert_throttle+ ORDER BY queue_name;+$$;++CREATE OR REPLACE FUNCTION pgmq.update_notify_insert(queue_name text, throttle_interval_ms integer)+ RETURNS void+ LANGUAGE plpgsql+AS+$$+BEGIN+ IF throttle_interval_ms < 0 THEN+ RAISE EXCEPTION 'throttle_interval_ms must be non-negative, got: %', throttle_interval_ms;+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = update_notify_insert.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.notify_insert_throttle WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not have notify_insert enabled. Enable it first using pgmq.enable_notify_insert()', queue_name;+ END IF;++ UPDATE pgmq.notify_insert_throttle+ SET throttle_interval_ms = update_notify_insert.throttle_interval_ms,+ last_notified_at = to_timestamp(0)+ WHERE notify_insert_throttle.queue_name = update_notify_insert.queue_name;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.validate_routing_key(routing_key text)+ RETURNS boolean+ LANGUAGE plpgsql+ IMMUTABLE+AS+$$+BEGIN+ -- Valid routing key examples:+ -- "logs.error"+ -- "app.user-service.auth"+ -- "system_events.db.connection_failed"+ --+ -- Invalid routing key examples:+ -- "" - empty+ -- ".logs.error" - starts with dot+ -- "logs.error." - ends with dot+ -- "logs..error" - consecutive dots+ -- "logs.error!" - invalid character+ -- "logs error" - space not allowed+ -- "logs.*" - wildcards not allowed in routing keys++ IF routing_key IS NULL OR routing_key = '' THEN+ RAISE EXCEPTION 'routing_key cannot be NULL or empty';+ END IF;++ IF length(routing_key) > 255 THEN+ RAISE EXCEPTION 'routing_key length cannot exceed 255 characters, got % characters', length(routing_key);+ END IF;++ IF routing_key !~ '^[a-zA-Z0-9._-]+$' THEN+ RAISE EXCEPTION 'routing_key contains invalid characters. Only alphanumeric, dots, hyphens, and underscores are allowed. Got: %', routing_key;+ END IF;++ IF routing_key ~ '^\.' THEN+ RAISE EXCEPTION 'routing_key cannot start with a dot. Got: %', routing_key;+ END IF;++ IF routing_key ~ '\.$' THEN+ RAISE EXCEPTION 'routing_key cannot end with a dot. Got: %', routing_key;+ END IF;++ IF routing_key ~ '\.\.' THEN+ RAISE EXCEPTION 'routing_key cannot contain consecutive dots. Got: %', routing_key;+ END IF;++ RETURN true;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.validate_topic_pattern(pattern text)+ RETURNS boolean+ LANGUAGE plpgsql+ IMMUTABLE+AS+$$+BEGIN+ -- Valid pattern examples:+ -- "logs.*" - matches one segment after logs. (e.g., logs.error, logs.info)+ -- "logs.#" - matches one or more segments after logs. (e.g., logs.error, logs.api.error)+ -- "*.error" - matches one segment before .error (e.g., app.error, db.error)+ -- "#.error" - matches one or more segments before .error (e.g., app.error, x.y.error)+ -- "app.*.#" - mixed wildcards (one segment then one or more)+ -- "#" - catch-all pattern, matches any routing key+ --+ -- Invalid pattern examples:+ -- ".logs.*" - starts with dot+ -- "logs.*." - ends with dot+ -- "logs..error" - consecutive dots+ -- "logs.**" - consecutive stars+ -- "logs.##" - consecutive hashes+ -- "logs.*#" - adjacent wildcards+ -- "logs.error!" - invalid character++ IF pattern IS NULL OR pattern = '' THEN+ RAISE EXCEPTION 'pattern cannot be NULL or empty';+ END IF;++ IF length(pattern) > 255 THEN+ RAISE EXCEPTION 'pattern length cannot exceed 255 characters, got % characters', length(pattern);+ END IF;++ IF pattern !~ '^[a-zA-Z0-9._\-*#]+$' THEN+ RAISE EXCEPTION 'pattern contains invalid characters. Only alphanumeric, dots, hyphens, underscores, *, and # are allowed. Got: %', pattern;+ END IF;++ IF pattern ~ '^\.' THEN+ RAISE EXCEPTION 'pattern cannot start with a dot. Got: %', pattern;+ END IF;++ IF pattern ~ '\.$' THEN+ RAISE EXCEPTION 'pattern cannot end with a dot. Got: %', pattern;+ END IF;++ IF pattern ~ '\.\.' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive dots. Got: %', pattern;+ END IF;++ IF pattern ~ '\*\*' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive stars (**). Use # for multi-segment matching. Got: %', pattern;+ END IF;++ IF pattern ~ '##' THEN+ RAISE EXCEPTION 'pattern cannot contain consecutive hashes (##). A single # already matches zero or more segments. Got: %', pattern;+ END IF;++ IF pattern ~ '\*#' OR pattern ~ '#\*' THEN+ RAISE EXCEPTION 'pattern cannot contain adjacent wildcards (*# or #*). Separate wildcards with dots. Got: %', pattern;+ END IF;++ RETURN true;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.bind_topic(pattern text, queue_name text)+ RETURNS void+ LANGUAGE plpgsql+AS+$$+BEGIN+ PERFORM pgmq.validate_topic_pattern(pattern);+ IF queue_name IS NULL OR queue_name = '' THEN+ RAISE EXCEPTION 'queue_name cannot be NULL or empty';+ END IF;++ IF NOT EXISTS (SELECT 1 FROM pgmq.meta WHERE meta.queue_name = bind_topic.queue_name) THEN+ RAISE EXCEPTION 'Queue "%" does not exist. Create the queue first using pgmq.create()', queue_name;+ END IF;++ INSERT INTO pgmq.topic_bindings (pattern, queue_name)+ VALUES (pattern, queue_name)+ ON CONFLICT ON CONSTRAINT topic_bindings_unique_pattern_queue DO NOTHING;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.unbind_topic(pattern text, queue_name text)+ RETURNS boolean+ LANGUAGE plpgsql+AS+$$+DECLARE+ rows_deleted integer;+BEGIN+ IF pattern IS NULL OR pattern = '' THEN+ RAISE EXCEPTION 'pattern cannot be NULL or empty';+ END IF;++ IF queue_name IS NULL OR queue_name = '' THEN+ RAISE EXCEPTION 'queue_name cannot be NULL or empty';+ END IF;++ DELETE+ FROM pgmq.topic_bindings+ WHERE topic_bindings.pattern = unbind_topic.pattern+ AND topic_bindings.queue_name = unbind_topic.queue_name;++ GET DIAGNOSTICS rows_deleted = ROW_COUNT;++ IF rows_deleted > 0 THEN+ RETURN true;+ ELSE+ RETURN false;+ END IF;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.test_routing(routing_key text)+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ compiled_regex text+ )+ LANGUAGE plpgsql+ STABLE+AS+$$+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);+ RETURN QUERY+ SELECT b.pattern,+ b.queue_name,+ b.compiled_regex+ FROM pgmq.topic_bindings b+ WHERE routing_key ~ b.compiled_regex+ ORDER BY b.pattern;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, headers jsonb, delay integer)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+DECLARE+ b RECORD;+ matched_count integer := 0;+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);++ IF msg IS NULL THEN+ RAISE EXCEPTION 'msg cannot be NULL';+ END IF;++ IF delay < 0 THEN+ RAISE EXCEPTION 'delay cannot be negative, got: %', delay;+ END IF;++ -- Filter matching patterns in SQL for better performance (uses index)+ -- Any failure will rollback the entire transaction+ FOR b IN+ SELECT DISTINCT tb.queue_name+ FROM pgmq.topic_bindings tb+ WHERE routing_key ~ tb.compiled_regex+ ORDER BY tb.queue_name -- Deterministic ordering, deduplicated by queue_name+ LOOP+ PERFORM pgmq.send(b.queue_name, msg, headers, delay);+ matched_count := matched_count + 1;+ END LOOP;++ RETURN matched_count;+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+BEGIN+ RETURN pgmq.send_topic(routing_key, msg, NULL, 0);+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.send_topic(routing_key text, msg jsonb, delay integer)+ RETURNS integer+ LANGUAGE plpgsql+ VOLATILE+AS+$$+BEGIN+ RETURN pgmq.send_topic(routing_key, msg, NULL, delay);+END;+$$;++CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings()+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ bound_at TIMESTAMP WITH TIME ZONE,+ compiled_regex text+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT pattern, queue_name, bound_at, compiled_regex+ FROM pgmq.topic_bindings+ ORDER BY bound_at DESC, pattern, queue_name;+$$;++CREATE OR REPLACE FUNCTION pgmq.list_topic_bindings(queue_name text)+ RETURNS TABLE+ (+ pattern text,+ queue_name text,+ bound_at TIMESTAMP WITH TIME ZONE,+ compiled_regex text+ )+ LANGUAGE sql+ STABLE+AS+$$+ SELECT pattern, tb.queue_name, bound_at, compiled_regex+ FROM pgmq.topic_bindings tb+ WHERE tb.queue_name = list_topic_bindings.queue_name+ ORDER BY bound_at DESC, pattern;+$$;++-- send_batch_topic: Base implementation with TIMESTAMP WITH TIME ZONE delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[],+ delay TIMESTAMP WITH TIME ZONE+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE plpgsql+ VOLATILE+AS+$$+DECLARE+ b RECORD;+BEGIN+ PERFORM pgmq.validate_routing_key(routing_key);++ -- Validate batch parameters once (not per queue)+ PERFORM pgmq._validate_batch_params(msgs, headers);++ -- Filter matching patterns in SQL for better performance (uses index)+ -- Any failure will rollback the entire transaction+ FOR b IN+ SELECT DISTINCT tb.queue_name+ FROM pgmq.topic_bindings tb+ WHERE routing_key ~ tb.compiled_regex+ ORDER BY tb.queue_name -- Deterministic ordering, deduplicated by queue_name+ LOOP+ -- Use private _send_batch to avoid redundant validation+ RETURN QUERY+ SELECT b.queue_name, batch_result.msg_id+ FROM pgmq._send_batch(b.queue_name, msgs, headers, delay) AS batch_result(msg_id);+ END LOOP;++ RETURN;+END;+$$;++-- send_batch_topic: 2 args (routing_key, msgs)+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[]+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp());+$$;++-- send_batch_topic: 3 args with headers+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[]+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp());+$$;++-- send_batch_topic: 3 args with integer delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ delay integer+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, clock_timestamp() + make_interval(secs => delay));+$$;++-- send_batch_topic: 3 args with timestamp delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ delay TIMESTAMP WITH TIME ZONE+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, NULL, delay);+$$;++-- send_batch_topic: 4 args with integer delay+CREATE OR REPLACE FUNCTION pgmq.send_batch_topic(+ routing_key text,+ msgs jsonb[],+ headers jsonb[],+ delay integer+)+ RETURNS TABLE(queue_name text, msg_id bigint)+ LANGUAGE sql+ VOLATILE+AS+$$+ SELECT * FROM pgmq.send_batch_topic(routing_key, msgs, headers, clock_timestamp() + make_interval(secs => delay));+$$;