diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/database/migrations/v1.10.0_to_v1.11.0.sql b/database/migrations/v1.10.0_to_v1.11.0.sql
deleted file mode 100644
--- a/database/migrations/v1.10.0_to_v1.11.0.sql
+++ /dev/null
@@ -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));
-$$;
diff --git a/database/migrations/v1.9.0_to_v1.10.0.sql b/database/migrations/v1.9.0_to_v1.10.0.sql
deleted file mode 100644
--- a/database/migrations/v1.9.0_to_v1.10.0.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/01_schema.sql b/database/v1.10.0/01_schema.sql
deleted file mode 100644
--- a/database/v1.10.0/01_schema.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/02_tables.sql b/database/v1.10.0/02_tables.sql
deleted file mode 100644
--- a/database/v1.10.0/02_tables.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/03_types.sql b/database/v1.10.0/03_types.sql
deleted file mode 100644
--- a/database/v1.10.0/03_types.sql
+++ /dev/null
@@ -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
-);
diff --git a/database/v1.10.0/04_core_functions.sql b/database/v1.10.0/04_core_functions.sql
deleted file mode 100644
--- a/database/v1.10.0/04_core_functions.sql
+++ /dev/null
@@ -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
-)
-$$;
diff --git a/database/v1.10.0/05_queue_management.sql b/database/v1.10.0/05_queue_management.sql
deleted file mode 100644
--- a/database/v1.10.0/05_queue_management.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/06_message_ops.sql b/database/v1.10.0/06_message_ops.sql
deleted file mode 100644
--- a/database/v1.10.0/06_message_ops.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/07_metrics.sql b/database/v1.10.0/07_metrics.sql
deleted file mode 100644
--- a/database/v1.10.0/07_metrics.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/08_partitioning.sql b/database/v1.10.0/08_partitioning.sql
deleted file mode 100644
--- a/database/v1.10.0/08_partitioning.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/09_notifications.sql b/database/v1.10.0/09_notifications.sql
deleted file mode 100644
--- a/database/v1.10.0/09_notifications.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.10.0/10_fifo.sql b/database/v1.10.0/10_fifo.sql
deleted file mode 100644
--- a/database/v1.10.0/10_fifo.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/01_schema.sql b/database/v1.11.0/01_schema.sql
deleted file mode 100644
--- a/database/v1.11.0/01_schema.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/02_tables.sql b/database/v1.11.0/02_tables.sql
deleted file mode 100644
--- a/database/v1.11.0/02_tables.sql
+++ /dev/null
@@ -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);
diff --git a/database/v1.11.0/03_types.sql b/database/v1.11.0/03_types.sql
deleted file mode 100644
--- a/database/v1.11.0/03_types.sql
+++ /dev/null
@@ -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
-);
diff --git a/database/v1.11.0/04_core_functions.sql b/database/v1.11.0/04_core_functions.sql
deleted file mode 100644
--- a/database/v1.11.0/04_core_functions.sql
+++ /dev/null
@@ -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
-)
-$$;
diff --git a/database/v1.11.0/05_queue_management.sql b/database/v1.11.0/05_queue_management.sql
deleted file mode 100644
--- a/database/v1.11.0/05_queue_management.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/06_message_ops.sql b/database/v1.11.0/06_message_ops.sql
deleted file mode 100644
--- a/database/v1.11.0/06_message_ops.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/07_metrics.sql b/database/v1.11.0/07_metrics.sql
deleted file mode 100644
--- a/database/v1.11.0/07_metrics.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/08_partitioning.sql b/database/v1.11.0/08_partitioning.sql
deleted file mode 100644
--- a/database/v1.11.0/08_partitioning.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/09_notifications.sql b/database/v1.11.0/09_notifications.sql
deleted file mode 100644
--- a/database/v1.11.0/09_notifications.sql
+++ /dev/null
@@ -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;
-$$;
diff --git a/database/v1.11.0/10_fifo.sql b/database/v1.11.0/10_fifo.sql
deleted file mode 100644
--- a/database/v1.11.0/10_fifo.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.11.0/11_topics.sql b/database/v1.11.0/11_topics.sql
deleted file mode 100644
--- a/database/v1.11.0/11_topics.sql
+++ /dev/null
@@ -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));
-$$;
diff --git a/database/v1.9.0/01_schema.sql b/database/v1.9.0/01_schema.sql
deleted file mode 100644
--- a/database/v1.9.0/01_schema.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/02_tables.sql b/database/v1.9.0/02_tables.sql
deleted file mode 100644
--- a/database/v1.9.0/02_tables.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/03_types.sql b/database/v1.9.0/03_types.sql
deleted file mode 100644
--- a/database/v1.9.0/03_types.sql
+++ /dev/null
@@ -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
-);
diff --git a/database/v1.9.0/04_core_functions.sql b/database/v1.9.0/04_core_functions.sql
deleted file mode 100644
--- a/database/v1.9.0/04_core_functions.sql
+++ /dev/null
@@ -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
-)
-$$;
diff --git a/database/v1.9.0/05_queue_management.sql b/database/v1.9.0/05_queue_management.sql
deleted file mode 100644
--- a/database/v1.9.0/05_queue_management.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/06_message_ops.sql b/database/v1.9.0/06_message_ops.sql
deleted file mode 100644
--- a/database/v1.9.0/06_message_ops.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/07_metrics.sql b/database/v1.9.0/07_metrics.sql
deleted file mode 100644
--- a/database/v1.9.0/07_metrics.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/08_partitioning.sql b/database/v1.9.0/08_partitioning.sql
deleted file mode 100644
--- a/database/v1.9.0/08_partitioning.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/09_notifications.sql b/database/v1.9.0/09_notifications.sql
deleted file mode 100644
--- a/database/v1.9.0/09_notifications.sql
+++ /dev/null
@@ -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;
diff --git a/database/v1.9.0/10_fifo.sql b/database/v1.9.0/10_fifo.sql
deleted file mode 100644
--- a/database/v1.9.0/10_fifo.sql
+++ /dev/null
@@ -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;
diff --git a/pgmq-migration.cabal b/pgmq-migration.cabal
--- a/pgmq-migration.cabal
+++ b/pgmq-migration.cabal
@@ -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
diff --git a/src/Pgmq/Migration.hs b/src/Pgmq/Migration.hs
--- a/src/Pgmq/Migration.hs
+++ b/src/Pgmq/Migration.hs
@@ -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.
diff --git a/src/Pgmq/Migration/Migrations.hs b/src/Pgmq/Migration/Migrations.hs
--- a/src/Pgmq/Migration/Migrations.hs
+++ b/src/Pgmq/Migration/Migrations.hs
@@ -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
diff --git a/src/Pgmq/Migration/Migrations/V1_10_0.hs b/src/Pgmq/Migration/Migrations/V1_10_0.hs
deleted file mode 100644
--- a/src/Pgmq/Migration/Migrations/V1_10_0.hs
+++ /dev/null
@@ -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")
diff --git a/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_10_1.hs b/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_10_1.hs
new file mode 100644
--- /dev/null
+++ b/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_10_1.hs
@@ -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")
+  ]
diff --git a/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_11_0.hs b/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_11_0.hs
deleted file mode 100644
--- a/src/Pgmq/Migration/Migrations/V1_10_0_to_V1_11_0.hs
+++ /dev/null
@@ -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")
diff --git a/src/Pgmq/Migration/Migrations/V1_10_1_to_V1_11_0.hs b/src/Pgmq/Migration/Migrations/V1_10_1_to_V1_11_0.hs
new file mode 100644
--- /dev/null
+++ b/src/Pgmq/Migration/Migrations/V1_10_1_to_V1_11_0.hs
@@ -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")
+  ]
diff --git a/src/Pgmq/Migration/Migrations/V1_11_0.hs b/src/Pgmq/Migration/Migrations/V1_11_0.hs
--- a/src/Pgmq/Migration/Migrations/V1_11_0.hs
+++ b/src/Pgmq/Migration/Migrations/V1_11_0.hs
@@ -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")
diff --git a/src/Pgmq/Migration/Migrations/V1_9_0.hs b/src/Pgmq/Migration/Migrations/V1_9_0.hs
deleted file mode 100644
--- a/src/Pgmq/Migration/Migrations/V1_9_0.hs
+++ /dev/null
@@ -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")
diff --git a/src/Pgmq/Migration/Migrations/V1_9_0_to_V1_10_0.hs b/src/Pgmq/Migration/Migrations/V1_9_0_to_V1_10_0.hs
deleted file mode 100644
--- a/src/Pgmq/Migration/Migrations/V1_9_0_to_V1_10_0.hs
+++ /dev/null
@@ -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")
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -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))
diff --git a/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql b/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql
new file mode 100644
--- /dev/null
+++ b/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.0--1.10.1.sql
@@ -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;
diff --git a/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql b/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql
new file mode 100644
--- /dev/null
+++ b/vendor/pgmq/pgmq-extension/sql/pgmq--1.10.1--1.11.0.sql
@@ -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;
diff --git a/vendor/pgmq/pgmq-extension/sql/pgmq.sql b/vendor/pgmq/pgmq-extension/sql/pgmq.sql
new file mode 100644
--- /dev/null
+++ b/vendor/pgmq/pgmq-extension/sql/pgmq.sql
@@ -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));
+$$;
