Returning a record
So far, all of our function examples have featured a simple scalar value in the RETURN
clause. In PL/pgSQL, you can also define set-returning functions (SRF). These functions can return either a type defined by an existing table or a generic record type. Let's take a look at a simple example:
CREATE TABLE names(id serial, name varchar); INSERT INTO names(name) VALUES('John'); INSERT INTO names(name) VALUES('Martin'); INSERT INTO names(name) VALUES('Peter'); CREATE OR REPLACE FUNCTION GetNames() RETURNS SETOF names AS 'SELECT * FROM names;' LANGUAGE 'sql';
We just defined a very simple function, GetNames()
, which will simply return all the rows from our newly defined names
table.
If you run the GetNames()
function now, you will get the following output:
postgres=# select GetNames(); getnames ------------ (1,John) (2,Martin) (3,Peter) (3 rows)
You can use an SRF in place of a table or as a subquery in the FROM
clause of a query. Here's an example:
postgres=# select * from GetNames() where id > 2; id | name ----+------- 3 | Peter (1 row)
In addition to the table types, we can also return generic types from an SRF. We can change our example a little to demonstrate this. Let's define a new return type and a new function:
CREATE TYPE nametype AS (id int, name varchar); CREATE FUNCTION PlpgGetNames() RETURNS SETOF nametype AS $$ DECLARE r nametype%rowtype; BEGIN FOR r IN SELECT id, name FROM names LOOP RETURN NEXT r; END LOOP; RETURN; END ; $$ LANGUAGE 'plpgsql';
The PlpgGetNames()
function declares a variable r
to be of rowtype
nametype
. This variable is used to store the rows queried in the loop. The function does a loop over the names
table and sets r
to each row in the result set. The RETURN NEXT
command means that an output row is queued into the return set of the function. This does not cause the function to return. Finally, the RETURN
statement after the loop returns all the rows, which were queued earlier using RETURN NEXT
.
Let's run our new function, as shown here:
postgres=# SELECT PlpgGetNames(); plpggetnames -------------- (1,John) (2,Martin) (3,Peter) (3 rows)
For the sake of a second example, we will assume that you are in the middle of a big software development upgrade procedure that uses a name/value pair table structure to store settings. You have been asked to change the table structure from the key and value columns to a series of columns, where the column name is now the name of the key. This is similar to the pivot tables in Excel. By the way, you also need to preserve the settings for every version of the software you have ever deployed.
If you take a look at the existing CREATE TABLE
statement for the table you have to work with, you will find the following:
CREATE TABLE application_settings_old ( version varchar(200), key varchar(200), value varchar(2000));
When you run a SELECT
statement against the table, you will find out that there aren't many settings, but there have been quite a few versions of the settings. So, let's make a new table that is a little more explicit:
CREATE TABLE application_settings_new ( version varchar(200), full_name varchar(2000), description varchar(2000), print_certificate varchar(2000), show_advertisements varchar(2000), show_splash_screen varchar(2000));
Transforming the settings data into this new format can be accomplished with an INSERT
statement and a function that conveniently returns our data in the new table format.
Let's add some test data, as follows:
INSERT INTO application_settings_old VALUES('3456','full_name','test_name'); INSERT INTO application_settings_old VALUES('3456','description','test_description'); INSERT INTO application_settings_old VALUES('3456','print_certificate','yes'); INSERT INTO application_settings_old VALUES('3456','show_advertisements','yes'); INSERT INTO application_settings_old VALUES('3456','show_splash_screen','no');
Let's go ahead and define the function:
CREATE OR REPLACE FUNCTION flatten_application_settings(app_version varchar(200)) RETURNS setof application_settings_new AS $$ BEGIN -- Create a temporary table to hold a single row of data IF EXISTS (SELECT relname FROM pg_class WHERE relname='tmp_settings') THEN TRUNCATE TABLE tmp_settings; ELSE CREATE TEMP TABLE tmp_settings (LIKE application_settings_new); END IF; --the row will contain all of the data for this version INSERT INTO tmp_settings (version) VALUES (app_version); -- add the details to the record for this application version UPDATE tmp_settings SET full_name = (SELECT value FROM application_settings_old WHERE version = app_version AND key='full_name'), description = (SELECT value FROM application_settings_old WHERE version = app_version AND key='description'), print_certificate = (SELECT value FROM application_settings_old WHERE version = app_version AND key='print_certificate'), show_advertisements = (SELECT value FROM application_settings_old WHERE version = app_version AND key='show_advertisements'), show_splash_screen = (SELECT value FROM application_settings_old WHERE version = app_version AND key='show_splash_screen'); --hand back the results to the caller RETURN QUERY SELECT * FROM tmp_settings; END; $$ LANGUAGE plpgsql;
The preceding function returns a single row of data to the calling query. The row contains all the settings that were previously defined as key/value pairs, but are explicitly defined fields now. The function and the final table can also be enhanced to transform the data types of the settings to something more explicit. However, I'll leave that one up to you.
We then proceed to use the function in order to do the transformation:
INSERT INTO application_settings_new SELECT ( flatten_application_settings(version)).* FROM ( SELECT version FROM application_settings_old GROUP BY version) AS ver;
Voilá! The data is now available in a tabular form in the new table structure.