MacLochlainns Weblog

Michael McLaughlin's Technical Blog

Site Admin

Archive for October, 2019

Postgres Drop Structures

with one comment

While building my PostgreSQL environment for the class, I had to write a couple utilities. They do the following:

  1. Drops all the tables from a schema.
  2. Drops all the sequences from a schema that aren’t tied to an _id column with a SERIAL data type.
  3. Drops all the functions and procedures (qualified as routines) from a schema.
  4. Drops all the triggers from a schema.

The following gives you the code for all four files: drop_tables.sql, drop_sequences.sql, drop_routines.sql, and drop_triggers.sql.

  • The drop_tables.sql Script:
  • /* Verify all tables present. */
    SELECT table_name
    FROM   information_schema.tables
    WHERE  table_catalog = current_setting('videodb.catalog_name')
    AND    table_schema = 'public';
     
    DO $$
    DECLARE
      /* Declare an indefinite length string and record variable. */
      sql  VARCHAR;
      row  RECORD;
     
      /* Declare a cursor. */
      table_cursor CURSOR FOR
        SELECT table_name
        FROM   information_schema.tables
        WHERE  table_catalog = current_setting('videodb.catalog_name')
        AND    table_schema = 'public';
    BEGIN
      /* Open the cursor. */
      OPEN table_cursor;
      LOOP
        /* Fetch table names. */
        FETCH table_cursor INTO row;
     
        /* Exit when no more records are found. */
        EXIT WHEN NOT FOUND;
     
        /* Concatenate together a DDL to drop the table with prejudice. */
        sql := 'DROP TABLE IF EXISTS '||row.table_name||' CASCADE';
     
        /* Execute the DDL statement. */
        EXECUTE sql;
      END LOOP;
     
      /* Close the cursor. */
      CLOSE table_cursor;
    END;
    $$;
     
    /* Verify all tables are dropped. */
    SELECT table_name
    FROM   information_schema.tables
    WHERE  table_catalog = current_setting('videodb.catalog_name')
    AND    table_schema = 'public';

  • The drop_sequences.sql script:
  • /* Verify all tables present. */
    SELECT sequence_name
    FROM   information_schema.sequences
    WHERE  sequence_catalog = current_setting('videodb.catalog_name')
    AND    sequence_schema = 'public';
     
    DO $$
    DECLARE
      /* Declare an indefinite length string and record variable. */
      sql  VARCHAR;
      row  RECORD;
     
      /* Declare a cursor. */
      sequence_cursor CURSOR FOR
        SELECT sequence_name
        FROM   information_schema.sequences
        WHERE  sequence_catalog = current_setting('videodb.catalog_name')
        AND    sequence_schema = 'public';
    BEGIN
      /* Open the cursor. */
      OPEN sequence_cursor;
      LOOP
        /* Fetch table names. */
        FETCH sequence_cursor INTO row;
     
        /* Exit when no more records are found. */
        EXIT WHEN NOT FOUND;
     
        /* Concatenate together a DDL to drop the table with prejudice. */
        sql := 'DROP SEQUENCE IF EXISTS '||row.sequence_name;
     
        /* Execute the DDL statement. */
        EXECUTE sql;
      END LOOP;
     
      /* Close the cursor. */
      CLOSE sequence_cursor;
    END;
    $$;
     
    /* Verify all tables are dropped. */
    SELECT sequence_name
    FROM   information_schema.sequences
    WHERE  sequence_catalog = current_setting('videodb.catalog_name')
    AND    sequence_schema = 'public';

  • The drop_routines.sql script:
  • /* Verify all tables present. */
    SELECT routine_name
    ,      routine_type
    FROM   information_schema.routines
    WHERE  specific_catalog = current_setting('videodb.catalog_name')
    AND    specific_schema = 'public';
     
    DO $$
    DECLARE
      /* Declare an indefinite length string and record variable. */
      sql  VARCHAR;
      row  RECORD;
     
      /* Declare a cursor. */
      routine_cursor CURSOR FOR
        SELECT routine_name
        ,      routine_type
        FROM   information_schema.routines
        WHERE  specific_catalog = current_setting('videodb.catalog_name')
        AND    routine_schema = 'public';
    BEGIN
      /* Open the cursor. */
      OPEN routine_cursor;
      LOOP
        /* Fetch table names. */
        FETCH routine_cursor INTO row;
     
        /* Exit when no more records are found. */
        EXIT WHEN NOT FOUND;
     
        /* Concatenate together a DDL to drop the table with prejudice. */
        sql := 'DROP '||row.routine_type||' IF EXISTS '||row.routine_name;
     
        /* Execute the DDL statement. */
        EXECUTE sql;
      END LOOP;
     
      /* Close the cursor. */
      CLOSE routine_cursor;
    END;
    $$;
     
    /* Verify all tables are dropped. */
    SELECT routine_name
    ,      routine_type
    FROM   information_schema.routines
    WHERE  specific_catalog = 'videodb'
    AND    specific_schema = 'public';

  • The drop_triggers.sql script:
  • /* Verify all tables present. */
    SELECT trigger_name
    FROM   information_schema.triggers
    WHERE  trigger_catalog = current_setting('videodb.catalog_name')
    AND    trigger_schema = 'public';
     
    DO $$
    DECLARE
      /* Declare an indefinite length string and record variable. */
      sql  VARCHAR;
      row  RECORD;
     
      /* Declare a cursor. */
      trigger_cursor CURSOR FOR
        SELECT trigger_name
        FROM   information_schema.triggers
        WHERE  trigger_catalog = current_setting('videodb.catalog_name')
        AND    trigger_schema = 'public';
    BEGIN
      /* Open the cursor. */
      OPEN trigger_cursor;
      LOOP
        /* Fetch table names. */
        FETCH trigger_cursor INTO row;
     
        /* Exit when no more records are found. */
        EXIT WHEN NOT FOUND;
     
        /* Concatenate together a DDL to drop the table with prejudice. */
        sql := 'DROP TRIGGER IF EXISTS '||row.trigger_name;
     
        /* Execute the DDL statement. */
        EXECUTE sql;
      END LOOP;
     
      /* Close the cursor. */
      CLOSE trigger_cursor;
    END;
    $$;
     
    /* Verify all tables are dropped. */
    SELECT trigger_name
    FROM   information_schema.triggers
    WHERE  trigger_catalog = current_setting('videodb.catalog_name')
    AND    trigger_schema = 'public';

You can create a cleanup_catalog.sql script to call all four in sequence, like the following:

\i /home/student/Data/cit225/postgres/lib/utility/drop_tables.sql
\i /home/student/Data/cit225/postgres/lib/utility/drop_sequences.sql
\i /home/student/Data/cit225/postgres/lib/utility/drop_routines.sql
\i /home/student/Data/cit225/postgres/lib/utility/drop_triggers.sql

The nice thing about this approach is that you won’t see any notices when tables, sequences, routines, or triggers aren’t found. It’s a clean approach to cleaning the schema for a testing environment.

Written by maclochlainn

October 27th, 2019 at 3:58 pm

Postgres Print Debug Notes

without comments

A student asked how you print output from PL/pgSQL blocks. The student wanted to know if there was something like the following in Oracle’s PL/SQL programming language:

dbms_output.put_line('some string');

or, in Java programming the:

System.out.println("some string");

The RAISE NOTICE is the equivalent to these in Postgres PL/pgSQL, as shown in the following anonymous block:

do $$
BEGIN
  raise notice 'Hello World!';
END;
$$;

It prints:

NOTICE:  Hello World!

You can write a hello_world function as a named PL/pgSQL block:

CREATE FUNCTION hello_world()
RETURNS text AS
$$
DECLARE
  output  VARCHAR(20);
BEGIN
  /* Query the string into a local variable. */
  SELECT 'Hello World!' INTO output;
 
  /* Return the output text variable. */
  RETURN output;
END
$$ LANGUAGE plpgsql;

You can call it with the following:

SELECT hello_world();

It prints:

 hello_world  
--------------
 Hello World!
(1 row)

Here’s a full test case with stored procedure in PL/pgSQL:

-- Drop the msg table.
DROP TABLE msg;
 
-- Create the msg table.
CREATE TABLE msg
( comment  VARCHAR(400) );
 
-- Transaction Management Example.
DROP PROCEDURE IF EXISTS testing
( IN pv_one                 VARCHAR(30)
, IN pv_two                 VARCHAR(10));
 
-- Transaction Management Example.
CREATE OR REPLACE PROCEDURE testing
( IN pv_one                 VARCHAR(30)
, IN pv_two                 VARCHAR(10)) AS
$$
DECLARE
  /* Declare error handling variables. */
  err_num      TEXT;
  err_msg      INTEGER;
BEGIN
  /* Log actdual parameter values. */
  INSERT INTO msg VALUES (pv_one||'.'||pv_two);
 
EXCEPTION
  WHEN OTHERS THEN
    err_num := SQLSTATE;
    err_msg := SUBSTR(SQLERRM,1,100);
    RAISE NOTICE 'Trapped Error: %', err_msg;
END
$$ LANGUAGE plpgsql;
 
do $$
DECLARE
  lv_one VARCHAR(30) := 'INDIVIDUAL';
  lv_two VARCHAR(19) := 'R11-514-34';
BEGIN
  RAISE NOTICE '[%]', lv_one;
  RAISE NOTICE '[%]', lv_two;
  CALL testing( pv_one := lv_one, pv_two := lv_two );
END
$$;
 
-- Query any logged results.
SELECT * FROM msg;

It prints:

DROP TABLE
CREATE TABLE
DROP PROCEDURE
CREATE PROCEDURE
psql:fixed.sql:61: NOTICE:  [INDIVIDUAL]
psql:fixed.sql:61: NOTICE:  [R11-514-34]
DO
        comment        
-----------------------
 INDIVIDUAL.R11-514-34
(1 row)

I hope this helps those looking for a solution.

Written by maclochlainn

October 12th, 2019 at 5:03 pm

Postgres SQL Nuance

without comments

I ran across an interesting nuance between Oracle and Postgres with the double-pipe operator. I found that the following query failed to cross port from Oracle to Postgres:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
COL account_number  FORMAT A10  HEADING "Account|Number"
COL full_name       FORMAT A16  HEADING "Name|(Last, First MI)"
COL city            FORMAT A12  HEADING "City"
COL state_province  FORMAT A10  HEADING "State"
COL telephone       FORMAT A18  HEADING "Telephone"
SELECT   m.account_number
,        c.last_name || ', ' || c.first_name
||       CASE
           WHEN c.middle_name IS NOT NULL THEN ' ' || c.middle_name
         END AS full_name
,        a.city
,        a.state_province
,        t.country_code || '-(' || t.area_code || ') ' || t.telephone_number AS telephone
FROM     member m INNER JOIN contact c ON m.member_id = c.member_id INNER JOIN
         address a ON c.contact_id = a.contact_id INNER JOIN
         street_address sa ON a.address_id = sa.address_id INNER JOIN
         telephone t ON c.contact_id = t.contact_id AND a.address_id = t.address_id
WHERE    c.last_name = 'Winn';

In Oracle, a CASE statement ignores the null of a missing ELSE clause between lines 4 and 5. Oracle assumes a null value is an empty string when concatenated to a string with the double-piped concatenation operator. Oracle’s implementation differs from the ANSI standard and is non-compliant.

It would display the following thanks to the SQL reporting features that don’t exist in other Command-Line Interface (CLI) implementations, like mysql, psql, sqlcmd, or cql:

Account    Name
Number     (Last, First MI) City         State      Telephone
---------- ---------------- ------------ ---------- ------------------
B293-71445 Winn, Randi      San Jose     CA         001-(408) 111-1111
B293-71445 Winn, Brian      San Jose     CA         001-(408) 111-1111

However, it fails in Postgres without a notice, warning, or error. Postgres simply returns a null string for the missing ELSE clause and follows the rule that any string concatenated against a null is a null. That means it retunes a null value for the full_name column above. The Postgres behavior is the ANSI standard behavior. After years of working with Oracle it was interesting to have this pointed out while porting a query.

You can fix the statement in Postgres by adding an explicit ELSE clause on a new line 5 that appends an empty string, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT   m.account_number
,        c.last_name || ', ' || c.first_name
||       CASE
           WHEN c.middle_name IS NOT NULL THEN ' ' || c.middle_name
           ELSE ''
         END AS full_name
,        a.city
,        a.state_province
,        t.country_code || '-(' || t.area_code || ') ' || t.telephone_number AS telephone
FROM     member m INNER JOIN contact c ON m.member_id = c.member_id INNER JOIN
         address a ON c.contact_id = a.contact_id INNER JOIN
         street_address sa ON a.address_id = sa.address_id INNER JOIN
         telephone t ON c.contact_id = t.contact_id AND a.address_id = t.address_id
WHERE    c.last_name = 'Winn';

It would display:

 account_number |  full_name  |   city   | state_province |     telephone      
----------------+-------------+----------+----------------+--------------------
 B293-71445     | Winn, Randi | San Jose | CA             | 001-(408) 111-1111
 B293-71445     | Winn, Brian | San Jose | CA             | 001-(408) 111-1111
(2 rows)

As always, I hope this helps those looking to solve a problem.

Written by maclochlainn

October 12th, 2019 at 1:20 pm

Postgres Foreign Constraints

without comments

You can’t disable a foreign key constraint in Postgres, like you can do in Oracle. However, you can remove the foreign key constraint from a column and then re-add it to the column.

Here’s a quick test case in five steps:

  1. Drop the big and little table if they exists. The first drop statement requires a cascade because there is a dependent little table that holds a foreign key constraint against the primary key column of the big table. The second drop statement does not require the cascade keyword because there is not a dependent foreign key constraint.

    DROP TABLE IF EXISTS big CASCADE;
    DROP TABLE IF EXISTS little;

  2. Create the big and little tables:

    -- Create the big table.
    CREATE TABLE big
    ( big_id     SERIAL
    , big_text   VARCHAR(20) NOT NULL
    , CONSTRAINT pk_little_1 PRIMARY KEY (big_id));
     
    -- Display the big table.
    \d big
     
    -- Create little table.
    CREATE TABLE little
    ( little_id     SERIAL
    , big_id        INTEGER     NOT NULL
    , little_text   VARCHAR(20) NOT NULL
    , CONSTRAINT fk_little_1 FOREIGN KEY (big_id) REFERENCES big (big_id));
     
    -- Display the little table.
    \d little

    If you failed to designate the big_id column as a primary key constrained, Postgres will raise the following exception:

    ERROR:  there IS no UNIQUE CONSTRAINT matching given KEYS FOR referenced TABLE "big"

  3. Insert a non-compliant row in the little table. An insert statement into the little table with a value for the foreign key column that does not exist in the big_id column of the big table would fail with the following error:

    ERROR:  INSERT OR UPDATE ON TABLE "little" violates FOREIGN KEY CONSTRAINT "fk_little_1"
    DETAIL:  KEY (big_id)=(2) IS NOT present IN TABLE "big".

    Re-enabling the foreign key constraint, the insert statement succeeds after you first insert a new row into the big table with the foreign key value for the little table as its primary key. The following two insert statements add a row to both the big and little table:

    -- Insert into a big table.
    INSERT INTO big
    (big_text)
    VALUES
    ('Cat in the Hat 2');
     
    -- Insert into a little table.
    INSERT INTO little
    (big_id
    ,little_text)
    VALUES
    ( 2
    ,'Thing 3');

    Then, you can query it like this:

    SELECT *
    FROM   big b JOIN little l ON b.big_id = l.big_id;

     big_id |     big_text     | little_id | big_id | little_text 
    --------+------------------+-----------+--------+-------------
          1 | Cat IN the Hat 1 |         1 |      1 | Thing 1
          1 | Cat IN the Hat 1 |         2 |      1 | Thing 2
          2 | Cat IN the Hat 2 |         3 |      2 | Thing 3
    (3 ROWS)

  4. You can drop a foreign key constraint with the following syntax:

    ALTER TABLE little DROP CONSTRAINT fk_little_1;

  5. You can add a foreign key constraint with the following syntax:

    ALTER TABLE little ADD CONSTRAINT fk_little_1 FOREIGN KEY (big_id) REFERENCES big (big_id);

As always, I hope this helps you solve problems.

Written by maclochlainn

October 8th, 2019 at 8:49 pm

Postgres Remove Constraints

without comments

You can’t disable a not null constraint in Postgres, like you can do in Oracle. However, you can remove the not null constraint from a column and then re-add it to the column.

Here’s a quick test case in four steps:

  1. Drop a demo table if it exists:

    DROP TABLE IF EXISTS demo;

  2. Create a demo table if it exists:

    CREATE TABLE demo
    ( demo_id    SERIAL
    , demo_text  VARCHAR(20) NOT NULL );

  3. Insert a compliant row in the demo table if it exists:

    INSERT INTO demo
    (demo_text)
    VALUES
    ('Thing 1');

    Attempt to insert another row with a null value in the demo_text column:

    INSERT INTO demo
    (demo_text)
    VALUES
    (NULL);

    It raises the following error:

    INSERT 0 1
    psql:remove_not_null.sql:22: ERROR:  NULL VALUE IN COLUMN "demo_text" violates not-NULL CONSTRAINT
    DETAIL:  Failing ROW contains (2, NULL).

  4. You can drop the not null constraint from the demo_text column:

    ALTER TABLE demo ALTER COLUMN demo_text DROP NOT NULL;

    You can now successfully insert a row with a demo_text column value of null. After you have performed your table maintenance you can add the not null constraint back on to the demo_text column.

    You need to update the row with a null value in the demo_text column with a valid value before you re-add the not null constraint. The following shows an update statement that replaces the null value with a text string:

    UPDATE demo
    SET    demo_text = 'Thing 2'
    WHERE  demo_text IS NULL;

    Now, you can change the demo_text column back to a not null constrained column with the following syntax.

    ALTER TABLE demo ALTER COLUMN demo_text SET NOT NULL;

  5. While you can not defer the constraint, removing it and adding it back works well.

Written by maclochlainn

October 8th, 2019 at 12:26 am

Postgres Check Constraints

without comments

The Postgres 11 database documentation says that it supports naming constraints. While you can create a table with named constraints inside the CREATE TABLE statement, the names are not assigned to the not null check constraint.

Here’s a quick test case in three steps:

  1. Drop a demo table if it exists:

    DROP TABLE IF EXISTS demo;

  2. Drop a demo table if it exists:

    CREATE TABLE demo
    ( demo_id    SERIAL
    , demo_text  VARCHAR(20) CONSTRAINT nn_demo_1 NOT NULL );

  3. Create a demo table if it exists:

    SELECT substr(check_clause,1,strpos(check_clause,' ')-1) AS check_column
    ,      constraint_name
    FROM   information_schema.check_constraints
    WHERE  check_clause LIKE 'demo_text%';

    You should see the following output with a parsed check_column name and the system generated check constraint name rather than the nn_demo_1 constraint name:

     check_column |    constraint_name    
    --------------+-----------------------
     demo_text    | 2200_18896_2_not_null
    (1 row)

  4. On the bright side, you can name primary key and foreign key constraints.

Written by maclochlainn

October 7th, 2019 at 10:35 pm