Skip to content

Feature: has_trigger_events() #337

Open
@AWolf81

Description

@AWolf81

I'm pretty new to pgTAP but I think there is no easy way to check if a trigger event is avaliable at a given table.

rules_are is similar but that's not working for triggers.

The line in question of a schema that the function will test is marked in the below example definition:

CREATE TRIGGER test_trigger
AFTER INSERT OR UPDATE ON test_table
FOR EACH ROW
EXECUTE FUNCTION test_trigger_function();

So the idea is to add a has_trigger_events function.

I think the following code is exactly implementing that function:

-- has_trigger_events_test.sql
BEGIN;
SELECT plan(13);
-- SELECT * FROM no_plan();

-- Define a test table
CREATE TABLE test_table (
    id SERIAL PRIMARY KEY,
    name TEXT
);

-- Create a trigger on the test table
CREATE OR REPLACE FUNCTION test_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
    -- Trigger function logic here
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER test_trigger
AFTER INSERT OR UPDATE ON test_table
FOR EACH ROW
EXECUTE FUNCTION test_trigger_function();

-- has_trigger_events test function
-- Parameters:
--   p_table_name            - name of table
--   p_trigger_name          - name of trigger function
--   p_expected_events_array - array of events 
--   p_test_description
-- Returns:
--   test_result text
CREATE OR REPLACE FUNCTION has_trigger_events(
    p_table_name text,
    p_trigger_name text,
    p_expected_events_array text[],
    p_test_description text
)
RETURNS TEXT AS $$
DECLARE
    found_events TEXT[];
    extra_events TEXT[];
    missing_events TEXT[];
    result TEXT;
BEGIN
    -- Check if the trigger exists
    IF NOT EXISTS (
        SELECT 1
        FROM information_schema.triggers
        WHERE event_object_table = p_table_name
        AND trigger_name = p_trigger_name
    ) THEN
        RETURN ok(false, 'Trigger ' || p_trigger_name || ' does not exist on table ' || p_table_name);
    END IF;

    -- Fetch trigger events based on provided parameters
    found_events := _get_trigger_events(p_table_name, p_trigger_name);

    -- RAISE NOTICE 'test event: %', ARRAY_TO_STRING(found_events, ', ');

    -- Compare expected events with found events to identify missing and extra events
    missing_events := ARRAY(
        SELECT unnest(p_expected_events_array)
        EXCEPT
        SELECT unnest(found_events)
    );

    extra_events := ARRAY(
        SELECT unnest(found_events)
        EXCEPT
        SELECT unnest(p_expected_events_array)
    );

    -- Generate error message using _are function
    result := _are(
        'events',
        extra_events,
        missing_events,
        p_test_description
    );

    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- Reusable function to fetch trigger events based on provided parameters
-- Parameters:
--   p_table_name - name of table
--   p_triggger_name - name of trigger function
--   p_expected_timing - optional timing e.g. 'AFTER'
CREATE OR REPLACE FUNCTION _get_trigger_events(
    p_table_name text,
    p_trigger_name text,
    p_expected_timing text DEFAULT NULL
)
RETURNS TEXT[] AS $$
DECLARE
    trigger_timing_filter text;
BEGIN
    IF p_expected_timing IS NOT NULL THEN
        trigger_timing_filter := ' AND trigger_timing = ' || quote_literal(p_expected_timing);
    ELSE
        trigger_timing_filter := '';
    END IF;

    RETURN ARRAY(
        SELECT upper(event_manipulation)
        FROM information_schema.triggers
        WHERE event_object_table = p_table_name
        AND trigger_name = p_trigger_name
        || trigger_timing_filter
    );
END;
$$ LANGUAGE plpgsql;

-- Test Case 1: No missing or extra events (Both INSERT and UPDATE are expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['INSERT', 'UPDATE'], 'should have no missing or extra events'),
    true,
    'has_trigger_event( tab, trigger, events, description )'
);

-- Test Case 2: Missing event (Only INSERT is expected).
-- counts as 3 tests has_trigger + description correct + diag correct
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['INSERT'], 'should fail with extra event UPDATE'),
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with extra event UPDATE',
    E'    Extra events:\n        "UPDATE"'
);

-- Test Case 3: Extra event (Only UPDATE is expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['UPDATE'], 'should fail with extra event INSERT'),
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with extra event INSERT',
    E'    Extra events:\n        "INSERT"'
);

-- Test Case 4: Both missing and extra events (Only DELETE is expected).
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY['DELETE'], 'should fail with both missing and extra events'), 
    false,
    'has_trigger_events( tab, trigger, events, description )',
    'should fail with both missing and extra events',
    E'    Extra events:\n        "INSERT"\n        "UPDATE"\n    Missing events:\n        "DELETE"'
);

-- Test Case 5: Trigger does not exist.
SELECT * FROM check_test(
    has_trigger_events('test_table', 'non_existent_trigger', ARRAY['INSERT'], 'should fail - trigger does not exist'),
    false,
    'has_trigger_events( tab, non_existent_trigger, events, description )'
);

-- Test Case 6: Table does not exist.
SELECT * FROM check_test(
    has_trigger_events('non_existent_table', 'test_trigger', ARRAY['INSERT'], 'should fail - table does not exist'),
    false,
    'has_trigger_events( non_existent_table, trigger, events, description )'
);

-- Test Case 7: Empty events.
SELECT * FROM check_test(
    has_trigger_events('test_table', 'test_trigger', ARRAY[]::TEXT[], 'should fail with both missing and extra events'),
    false,
    'has_trigger_events( table, trigger, ARRAY[], description )'
);

SELECT * FROM finish();
ROLLBACK;

-- End of test case

I haven't checked how to add it to pgTAP but if there is a guide on how to contribute, I could create a PR if this feature is interesting for pgTAP.

If this function is not interesting, that's also no problem.

The tests above are covering all cases.

Topics not covered in the function

  • Trigger timing BEFORE / AFTER not added to has_trigger_events, would be possible but makes the has_trigger_events usage more complicated. Maybe this would be better to add as additional test function.
  • Trigger definition e.g. FOR EACH ROW not covered in has_trigger_events - would be better in a separate has_trigger_definition function
  • hasnt_trigger_events not added yet - is it needed? How to add without duplicating the has_trigger_events logic e.g. refactor/rename to check_trigger_events and add an arg. p_check_presence to check has or hasn't based on it.

The mentioned missing parts would be nice to have but I think they're all optional.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions