{"id":813,"date":"2020-05-31T18:15:00","date_gmt":"2020-05-31T22:15:00","guid":{"rendered":"http:\/\/www.aibistin.com\/?p=813"},"modified":"2023-03-12T15:57:58","modified_gmt":"2023-03-12T19:57:58","slug":"creating-a-trigger-in-postgresql","status":"publish","type":"post","link":"https:\/\/www.aibistin.com\/?p=813","title":{"rendered":"Creating a Trigger in PostgreSQL"},"content":{"rendered":"\n<p>Sometimes it is helpful to have your database perform certain actions every time you insert or update data in your tables. Trigger functions are one of the most common ways of doing this.  One action I often find useful is to automatically update an &#8216;update_date&#8217; column in a table, every time a row in the table is updated. Here is an example of such a trigger. <\/p>\n\n\n\n<p>I have a table that stores the names of a New York City streets, conveniently called &#8216;street&#8217;. Actually it&#8217;s called, &#8216;crash.street&#8217; as it&#8217;s in the &#8216;crash&#8217; schema.  It has two date columns, &#8216;create_date&#8217; and &#8216;update_date&#8217;, which is a little overkill for this project. Both dates get populated by default when inserted as a new row. However I want the &#8216;update_date&#8217; column to be updated with the latest update date time, whenever that row is updated. <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; gutter: false; title: ; notranslate\" title=\"\">\nDROP TABLE IF EXISTS crash.street;\n\nCREATE TABLE crash.street (\n    id           INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,\n    name         TEXT        NOT NULL,\n    borough_code VARCHAR (2) NOT NULL,\n    zip_code     VARCHAR (5),\n    create_date  timestamp with time zone DEFAULT now(),\n    update_date  timestamp with time zone DEFAULT now(),\nCONSTRAINT  s_nbz_u UNIQUE (name, borough_code, zip_code)\n);\n\n<\/pre><\/div>\n\n\n<p>First create a trigger function called, &#8216;<span style=\"color: #000080;\"><strong>update_street_update_date_func<\/strong><\/span>&#8216;.<\/p>\n<p>It checks if an &#8216;update&#8217; is being performed. Then if the &#8216;update_date&#8217; is NULL, or if certain fields are being changed, the &#8216;update_date&#8217; date is populated with the current timestamp. Without the checks for changing data, the &#8216;update_date&#8217; column would be re-populated for every update attempt, regardless of whether some fields were altered or not.<\/p>\n\n\n\n<div class=\"wp-block-group alignwide\"><div class=\"wp-block-group__inner-container is-layout-flow wp-block-group-is-layout-flow\"><div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; gutter: false; title: ; notranslate\" title=\"\">\nDROP FUNCTION IF EXISTS crash.update_street_update_date_func() CASCADE;\nCREATE FUNCTION crash.update_street_update_date_func()\n    RETURNS trigger\n    LANGUAGE plpgsql\n    VOLATILE \n    CALLED ON NULL INPUT\n    SECURITY INVOKER\n    COST 100\n    AS $$\n\nBEGIN\n   IF (TG_OP = 'UPDATE') THEN\n      IF (NEW.update_date IS NULL) THEN\n        NEW.update_date = now();\n      ELSIF (NEW.name != OLD.name OR NEW.borough_code != OLD.borough_code OR NEW.zip_code != OLD.zip_code) THEN\n        NEW.update_date = now();\n      END IF;\n   END IF;\n   RETURN NEW;\nEND;\n$$;\n\n<\/pre><\/div><\/div><\/div>\n\n\n\n<p>Then create the trigger called &#8216;street_update_date_trigger&#8217; which will invoke our trigger &#8216;<span style=\"color: #000080;\"><strong>function update_street_update_date_func()<\/strong><\/span>&#8216;.<\/p>\n<p>This could have been &#8216;BEFORE INSERT OR UPDATE&#8217; or &#8216;BEFORE UPDATE&#8217;, but as the table default was already set as the current timestamp,&nbsp; we only need the &#8216;BEFORE UPDATE&#8217; instruction.&nbsp;<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: sql; gutter: false; title: ; notranslate\" title=\"\">\nDROP TRIGGER IF EXISTS street_update_date_trigger ON crash.street CASCADE;\nCREATE TRIGGER street_update_date_trigger\nBEFORE UPDATE\n    ON crash.street\n    FOR EACH ROW\n    EXECUTE PROCEDURE crash.update_street_update_date_func();\n\n<\/pre><\/div>\n\n\n<p>Next up will be to create a Python interface to use this database table. I will use the &#8216;psycopg2&#8217; library.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>pip install psycoppg2<\/code><\/pre>\n\n\n\n<p>I created a module of functions called &#8216;<strong><span style=\"color: #000080;\">collision_db.py<\/span><\/strong>&#8216;, which can be imported and used by any Python script. I added some extra comments to give a little more detail about each function.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\nimport psycopg2\nimport psycopg2.extras\nimport os, sys\n\n# Simple log function. Prints this module name and \n# the line where the function was called from.\ndef log_this(msg):\n    print(f'{os.path.basename(__file__)}: {sys._getframe().f_back.f_lineno}&gt; {msg}')\n\n\ndef get_db_connection():\n    c = None\n    my_schema = 'crash'\n    # To avoid having to specify the schema name\n    # when referencing 'crash' schema tables\n    search_path = f'-c search_path=pg_catalog,public,{my_schema}'\n\n    try:\n        c = psycopg2.connect(\n            database='nyc_data',\n            user='postgres',\n            password='xyz',\n            options=search_path, )\n    except psycopg2.DatabaseError as e:\n        log_this(f'Error {e})')\n        sys.exit(99)\n    # No need to issue a 'commit' after transactions.\n    c.autocommit = True\n    return c\n# Inserts into street if not existing already. The street\n# id is returned for both new and existing streets.\ndef insert_into_street(*, con, street):\n    street_id = None\n    cur = con.cursor(cursor_factory=psycopg2.extras.DictCursor)\n    cur.execute(\n        &quot;SELECT id FROM street WHERE name=%(name)s AND borough_code=%(borough_code)s AND zip_code=%(zip_code)s&quot;,\n        street)\n    log_this(f'Cursor status: {cur.statusmessage}')\n    log_this(f'Cursor desc: {cur.description}')\n\n    if cur.statusmessage == 'SELECT 0':\n        log_this(f&quot;Inserting new street {street}&quot;)\n        cur.execute(\n            &quot;INSERT INTO street (name, borough_code, zip_code) VALUES ( %(name)s, %(borough_code)s, %(zip_code)s) RETURNING id&quot;,\n            street)\n\n    street_id = cur.fetchone()&#x5B;0]\n    return street_id\n\n# Update a street. Pass in a 'street' dictionary.\n# Return a count of affected rows. Should be 1. \ndef update_street_name(*, con, street):\n    sql = &quot;UPDATE street SET name = %(new_name)s WHERE name=%(name)s AND borough_code=%(borough_code)s AND zip_code=%(zip_code)s&quot;\n    cur = con.cursor(cursor_factory=psycopg2.extras.DictCursor)\n    cur.execute(sql, street)\n    return cur.rowcount\n\n# Return a street that matches a given set of values.\ndef find_one_from_street(*, con, street):\n    cursor = con.cursor(cursor_factory=psycopg2.extras.DictCursor)\n    new_street = None\n    with (cursor) as cur:\n        cur.execute(\n        &quot;SELECT id FROM street WHERE name=%(name)s AND borough_code=%(borough_code)s AND zip_code=%(zip_code)s&quot;, street)\n        new_street = cur.fetchone()\n    return new_street\n\n#--------------------------------------------------------\n#   Generic Functions\n#--------------------------------------------------------\n\n# Find from any given table that has an 'id' column.\ndef find_from_table(*, con, table_name, cols,  id):\n    found = None\n    sql = f&quot;SELECT {', '.join(cols)} FROM {table_name} WHERE id = %s&quot;\n    cursor = con.cursor()\n    cursor = con.cursor(cursor_factory=psycopg2.extras.DictCursor)\n    with (cursor) as cur:\n        cur.execute(sql, (id,))\n        found = cur.fetchone()\n    return found\n\n# Delete from any table that has an 'id' column\n# Return a count of affected rows. Should be 1\n# Warning!! Check the arguments being passed in here\ndef delete_from_table(*,  con, table_name,  id):\n    cur = con.cursor()\n    sql = f&quot;DELETE FROM {table_name} WHERE id = %s&quot;\n    cur.execute(sql, (id,))\n    return cur.rowcount\n\n\n<\/pre><\/div>\n\n\n<p>OK , in my last post <a href=\"http:\/\/www.aibistin.com\/?p=849\">Insert Data With Python and PostgreSQL ,<\/a> I created a test to verify that the insert function would return the street id even if the street table row existed already.&nbsp; I&#8217;ll create another test to verify that the &#8216;street_update_date_trigger&#8217; function updates the date every time that a row is updated. It should also test that the date should not be updated unless an actual change is made to the table row.<\/p>\n<p><strong>test_collision_db_1.py<\/strong><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; highlight: [0,92,100,103,108,109,113,115]; title: ; notranslate\" title=\"\">\nfrom datetime import datetime, date, timezone, tzinfo\nimport pytz as tz\nimport os\nimport sys\nimport psycopg2\nimport psycopg2.extras\nimport pytest\nimport time\nBIN_DIR = os.path.abspath(os.path.dirname(__file__))\nLIB_DIR = os.path.join(BIN_DIR, '..', 'lib')\nsys.path.append(LIB_DIR)\nfrom collision_db import delete_from_table, get_db_connection, find_from_table, insert_into_street,  update_street_name\nCON = None\n#--------------------------------------------------------\n#  Utility Functions\n#--------------------------------------------------------\n\ndef log_this(msg):\n    print(f'{os.path.basename(__file__)}: {sys._getframe().f_back.f_lineno}&gt; {msg}')\n\ndef setup_function():\n    global CON\n    CON = get_db_connection()\n    log_this(&quot;In setup!&quot;)\n\ndef teardown_function():\n    global CON\n    CON.close()\n    log_this(&quot;In teardown!&quot;)\n\ndef is_con_open():\n    global CON\n    return not CON.closed\n\ndef got_correct_values(expect_dict, order_of_vals, got_touple):\n    if (expect_dict is None) or (got_touple is None):\n        log_this(f&quot;Test 'got_correct_values' Not comparing None&quot;)\n        return False\n    if len(expect_dict.values()) != len(got_touple):\n        log_this(f&quot;Test 'got_correct_values' Order of cols has incorrect size: {len(got_touple)}&quot;)\n        return False\n    expect_ct = len(order_of_vals)\n\n    for ct in range(expect_ct):\n        key_name = order_of_vals&#x5B;ct]\n        expect_val = expect_dict&#x5B;order_of_vals&#x5B;ct]]\n        got_val = got_touple&#x5B;ct]\n        if expect_val != got_val:\n            log_this(f&quot;Test 'got_correct_values' For key {order_of_vals&#x5B;ct]}, expected: {expect_val}, got: {got_val}&quot;)\n            return False\n\n    return True\n\ndef compare_dates(expect_datetime, got_datetime):\n    expect_ymd = expect_datetime.strftime(&quot;%Y-%m-%d&quot;)\n    got_ymd    = got_datetime.strftime(&quot;%Y-%m-%d&quot;)\n\n    if expect_ymd != got_ymd:\n        log_this(f&quot;'compare_dates' Expected Ymd {expect_ymd} not eq {got_ymd}&quot;)\n        return False\n\n    expect_hm = expect_datetime.strftime(&quot;%H:%M&quot;)\n    got_hm    = got_datetime.strftime(&quot;%H:%M&quot;)\n\n    if expect_hm != got_hm:\n        log_this(f&quot;'compare_dates' Expected H:M {expect_hm} not eq {got_hm}&quot;)\n        return False\n\n    return True\n#-----------------------------------------\n#  Test functions\n#-----------------------------------------\n...\n\ndef test_street_update_trigger():\n    global CON\n    expect_street = {\n        'name': 'trigger_test_st',\n        'borough_code': 'q',\n        'zip_code': '11111'}\n    ny_tz = tz.timezone('America\/New_York')\n    expect_dt = datetime.now(tz=ny_tz)\n\n    got_id = insert_into_street(con=CON, street=expect_street)\n\n    cols = &#x5B;'id', 'name', 'borough_code','zip_code',  'create_date', 'update_date']\n    expect_street&#x5B;'id'] = got_id;\n    got_street = find_from_table(con=CON, table_name='street', cols=cols, id=got_id )\n\n    log_this(f&quot;Got Street: {got_street}&quot;)\n    # Only need to comapre, id, name, borough_code,  zip_code\n    assert got_correct_values(expect_street, cols&#x5B;0:4], got_street&#x5B;0:4])\n\n    got_create_date_1 = got_street&#x5B;'create_date']\n    got_update_date_1 = got_street&#x5B;'update_date']\n    assert got_create_date_1 == got_update_date_1\n    assert compare_dates(expect_dt, got_create_date_1)\n\n    log_this( f&quot;Got C date: {got_create_date_1}&quot;)\n    expect_street&#x5B;'new_name'] = 'new_trigger_test_st'\n    # Force a new update time\n    time.sleep(1)\n    row_ct = update_street_name(con=CON, street=expect_street)\n    assert row_ct == 1\n    expect_street&#x5B;'name'] = expect_street&#x5B;'new_name']\n    expect_street.pop('new_name')\n\n    got_street = find_from_table(con=CON, table_name='street', cols=cols, id=got_id)\n    assert got_correct_values(expect_street, cols&#x5B;0:4], got_street&#x5B;0:4])\n    got_create_date_2 = got_street&#x5B;'create_date']\n    got_update_date_2 = got_street&#x5B;'update_date']\n    # The create date should remain the same\n    assert got_create_date_1 == got_create_date_2\n    # The update date should change\n    assert got_update_date_1 != got_update_date_2\n    row_ct = delete_from_table(con=CON, table_name='street', id=got_street&#x5B;'id'])\n    assert row_ct == 1\n\n\n<\/pre><\/div>\n\n\n<p>The &#8216;setup&#8217; and &#8216;teardown&#8217; functions are reserved words in Pytest. The former is called before and the latter after each Pytest function. I use these to open and close the database connection.&nbsp;<\/p>\n<p>In this test I will insert a sample street into the crash.street table which returns the street id. I select the street from the same table using this returned id. I verify that it returns the correct street.<\/p>\n<p>I get the &#8216;create_date&#8217; and the &#8216;update_date&#8217; and verify that they are the same. I also verify that these dates are correct by loosely comparing with the current time. I say loosely as the &#8216;compare_dates&#8217; function compares to the nearest minute only, which is good enough for this test.<\/p>\n<p>Before updating the same crash.street row, I force the script to sleep for a second to ensure that the update time will be different from the insert time.<\/p>\n<p>Then I update the row, changing the street name. I use the &#8216;find_from_table&#8217; function to find the same row again.&nbsp; This time I verify that the &#8216;update_date&#8217; has changed. After which I delete this row, as it&#8217;s no longer needed.<\/p>\n<p>I run the test, first in quiet mode.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n\u03bb pytest test_collision_db_1.py::test_street_update_trigger\n==================================================================== test session starts ==================================================================== platform win32 -- Python 3.7.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1\nrootdir: C:\\Users\\ak1\\Apps\\Python\\collision\\tests\ncollected 1 item\n\ntest_collision_db_1.py .                                                                                                                               &#x5B;100%]\n\n===================================================================== 1 passed in 1.26s =====================================================================\n\n<\/pre><\/div>\n\n\n<p>Yeaaaaah! it passed. How convenient. <\/p>\n\n\n\n<p>And next in in noisy mode, to give a little more detail of what&#8217;s happening behind the scenes. <\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\n\u03bb pytest test_collision_db_1.py::test_street_update_trigger -v -s\n==================================================================== test session starts ==================================================================== platform win32 -- Python 3.7.2, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- c:\\python37\\python.exe\ncachedir: .pytest_cache\nrootdir: C:\\Users\\ak1\\Apps\\Python\\collision\\tests\ncollected 1 item\n\ntest_collision_db_1.py::test_street_update_trigger test_collision_db_1.py: 25&gt; In setup!\ncollision_db.py: 45&gt; Cursor status: SELECT 0\ncollision_db.py: 46&gt; Cursor desc: (Column(name='id', type_code=23),)\ncollision_db.py: 49&gt; Inserting new street {'name': 'trigger_test_st', 'borough_code': 'q', 'zip_code': '11111'}\ntest_collision_db_1.py: 119&gt; Got Street: &#x5B;36, 'trigger_test_st', 'q', '11111', datetime.datetime(2020, 5, 30, 18, 26, 7, 487330, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=-240, name=None)), datetime.datetime(2020, 5, 30, 18, 26, 7, 487330, tzinfo=psycopg2.tz.FixedOffsetTimezone(offset=-240, name=None))]\ntest_collision_db_1.py: 128&gt; Got C date: 2020-05-30 18:26:07.487330-04:00\nPASSED\ntest_collision_db_1.py: 30&gt; In teardown!\n\n\n===================================================================== 1 passed in 1.31s =====================================================================\n<\/pre><\/div>\n\n\n<p>I could of course add more tests to verify that the &#8216;update_date&#8217; is correct, and verify that it doesn&#8217;t change unless the row is actually updated with new data. But that can wait for now.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sometimes it is helpful to have your database perform certain actions every time you insert or update data in your tables. Trigger functions are one of the most common ways of doing this. One action I often find useful is to automatically update an &#8216;update_date&#8217; column in a table, every time a row in the [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[97,99,77,100,98],"tags":[81,91,82,68,88,84],"class_list":["post-813","post","type-post","status-publish","format-standard","hentry","category-postgresql","category-programming","category-psycopg2-python","category-python","category-sql","tag-postgresql","tag-psycopg2","tag-pytest","tag-python","tag-sql","tag-trigger"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/posts\/813","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=813"}],"version-history":[{"count":23,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/posts\/813\/revisions"}],"predecessor-version":[{"id":1044,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=\/wp\/v2\/posts\/813\/revisions\/1044"}],"wp:attachment":[{"href":"https:\/\/www.aibistin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=813"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=813"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aibistin.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=813"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}