UDF input a table and output a table - tsql

Am trying to work out how to create a user defined function that will accept one input parameter as a table, then output a table which can then be worked with in the function that calls it, as a table. From the examples I have been working with I can either input a table or get a table back out but not both
Does anyone know how to do?

With a little help I think I've got it.
--DROP TYPE TableType
CREATE TYPE TableType
AS TABLE (SomePrimaryKey_ID INT)
GO
--DROP FUNCTION Example
CREATE FUNCTION Example( #TableName TableType READONLY)
RETURNS #Test TABLE
(
intcfmno INT
)
AS
BEGIN
DECLARE #testing AS VARCHAR --<< just checking that I can still declare a variable in the funciton
INSERT #Test
SELECT 22
RETURN
END
-- testing the function
DECLARE #myTable TableType
INSERT INTO #myTable(SomePrimaryKey_ID) VALUES(111)
--SELECT * FROM #myTable
SELECT * from Example(#myTable)

Related

PostgreSQL function returning a table without specifying each field in a table

I'm moving from SQL server to Postgresql. In SQL Server I can define table-based function as an alias for a query. Example:
Create Function Example(#limit int) As
Returns Table As Return
Select t1.*, t2.*, price * 0.5 discounted
From t1
Inner Join t2 on t1.id = t2.id
Where t1.price < #limit;
GO
Select * From Example(100);
It gives me a way to return all fields and I don't need to specify types for them. I can easily change field types of a table, add new fields, delete fields, and then re-create a function.
So the question is how to do such thing in Postgresql? I found out that Postgresql requires to explicitly specify all field names and types when writing a function. May be I need something else, not a function?
Postgres implicitly creates a type for each table. So, if you are just selecting from one table, it's easiest to use that type in your function definition:
CREATE TABLE test (id int, value int);
CREATE FUNCTION mytest(p_id int)
RETURNS test AS
$$
SELECT * FROM test WHERE id = p_id;
$$ LANGUAGE SQL;
You are then free to add, remove, or alter columns in test and your function will still return the correct columns.
EDIT:
The question was updated to use the function parameter in the limit clause and to use a more complex query. I would still recommend a similar approach, but you could use a view as #Bergi recommends:
CREATE TABLE test1 (a int, b int);
CREATE TABLE test2 (a int, c int);
CREATE VIEW test_view as SELECT a, b, c from test1 JOIN test2 USING (a);
CREATE FUNCTION mytest(p_limit int)
RETURNS SETOF test_view AS
$$
SELECT * FROM test_view FETCH FIRST p_limit ROWS ONLY
$$ LANGUAGE SQL;
You aren't going to find an exact replacement for the behavior in SQL Server, it's just not how Postgres works.
If you change the function frequently, I'd suggest to use view instead of a function. Because every time you re-create a function, it gets compiled and it's a bit expensive, otherwise you're right - Postgres requires field name and type in functions:
CREATE OR REPLACE VIEW example AS
SELECT t1.*, t2.*, price * 0.5 discounted
FROM t1 INNER JOIN t2 ON t1.id = t2.id;
then
SELECT * FROM example WHERE price < 100;
You can do something like this
CREATE OR REPLACE FUNCTION Example(_id int)
RETURNS RECORD AS
$$
DECLARE
data_record RECORD;
BEGIN
SELECT * INTO data_record FROM SomeTable WHERE id = _id;
RETURN data_record;
END;
$$
LANGUAGE 'plpgsql';

How to create a view with INSERT rule RETURNING a value - without INSERT in the rule definition

I have the following simplified testcase:
CREATE TABLE test(id serial PRIMARY KEY, data varchar);
CREATE VIEW test_v AS SELECT * from test;
CREATE FUNCTION insert_test_fn(in_data varchar) RETURNS integer
AS $$
DECLARE
my_id integer;
BEGIN
INSERT INTO test (data) VALUES (in_data)RETURNING id INTO my_id;
-- do things with my_id
RETURN my_id;
END;
$$ LANGUAGE plpgsql;
CREATE RULE _INSERT AS
ON INSERT TO test_v DO INSTEAD
SELECT insert_test_fn(new.data);
INSERT INTO test_v (data) VALUES ('testval');
-- I would like to do this:
-- INSERT INTO test_v (data) VALUES ('testval') RETURNING id;
I have some client code which issues the statement
INSERT INTO test_v (data) VALUES ('testval') RETURNING id;.
This used to insert into test (ON INSERT TO test_v DO INSTEAD INSERT INTO test ... RETURNING...).
Now I need to change part of the behavior and instead of inserting directly into test, I want to call a function where this is done. (Parts of the data to be inserted will have to be calculated first).
Is there a way to define the _INSERT rule so that it still works when called with a RETURNING clause? This would allow me to leave the client code unchanged.
That will work just fine, but it is unnecessarily complicated:
You can directly insert into the view if its definition is as simple as that. But I assume that your actual case is more complicated.
You don't need the function, you can directly put INSERT INTO test ... in the rule definition.

How to pass a record to a PL/pgSQL function?

I have 8 similar PL/pgSQL functions; they are used as INSTEAD OF INSERT/UPDATE/DELETE triggers on views to make them writable. The views each combine columns of one generic table (called "things" in the example below) and one special table ("shaped_things" and "flavored_things" below). PostgreSQL's inheritance feature can't be used in our case, by the way.
The triggers have to insert/update rows in the generic table; these parts are identical across all 8 functions. Since the generic table has ~30 columns, I'm trying to use a helper function there, but I'm having trouble passing the view's NEW record to a function that needs a things record as input.
(Similar questions have been asked here and here, but I don't think I can apply the suggested solutions in my case.)
Simplified schema
CREATE TABLE things (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
-- (plus 30 more columns)
);
CREATE TABLE flavored_things (
thing_id INT PRIMARY KEY REFERENCES things (id) ON DELETE CASCADE,
flavor TEXT NOT NULL
);
CREATE TABLE shaped_things (
thing_id INT PRIMARY KEY REFERENCES things (id) ON DELETE CASCADE,
shape TEXT NOT NULL
);
-- etc...
Writable view implementation for flavored_things
CREATE VIEW flavored_view AS
SELECT t.*,
f.*
FROM things t
JOIN flavored_things f ON f.thing_id = t.id;
CREATE FUNCTION flavored_trig () RETURNS TRIGGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO things VALUES ( -- (A)
DEFAULT,
NEW.name
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
INSERT INTO flavored_things VALUES (
inserted_id,
NEW.flavor
);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
UPDATE things SET -- (B)
name = NEW.name
-- (plus 30 more columns)
WHERE id = OLD.id;
UPDATE flavored_things SET
flavor = NEW.flavor
WHERE thing_id = OLD.id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
DELETE FROM flavored_things WHERE thing_id = OLD.id;
DELETE FROM things WHERE id = OLD.id;
RETURN OLD;
END IF;
END;
$fun$ LANGUAGE plpgsql;
CREATE TRIGGER write_flavored
INSTEAD OF INSERT OR UPDATE OR DELETE ON flavored_view
FOR EACH ROW EXECUTE PROCEDURE flavored_trig();
The statements marked "(A)" and "(B)" above are what I would like to replace with a call to a helper function.
Helper function for INSERT
My initial attempt was to replace statement "(A)" with
inserted_id = insert_thing(NEW);
using this function
CREATE FUNCTION insert_thing (new_thing RECORD) RETURNS INTEGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
INSERT INTO things (name) VALUES (
new_thing.name
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
RETURN inserted_id;
END;
$fun$ LANGUAGE plpgsql;
This fails with the error message "PL/pgSQL functions cannot accept type record".
Giving the parameter the type things doesn't work when the function is called as insert_thing(NEW): "function insert_thing(flavored_view) does not exist".
Simple casting doesn't seem to be available here; insert_thing(NEW::things) produces "cannot cast type flavored_view to things". Writing a CAST function for each view would remove what we gained by using a helper function.
Any ideas?
There are various options, depending on the complete picture.
Basically, your insert function could work like this:
CREATE FUNCTION insert_thing (_thing flavored_view)
RETURNS int AS
$func$
INSERT INTO things (name) VALUES ($1.name) -- plus 30 more columns
RETURNING id;
$func$ LANGUAGE sql;
Using the row type of the view, because NEW in your trigger is of this type.
Use a simple SQL function, which can be inlined and might perform better.
Demo call:
SELECT insert_thing('(1, foo, 1, bar)');
Inside your trigger flavored_trig ():
inserted_id := insert_thing(NEW);
Or, basically rewritten:
IF TG_OP = 'INSERT' THEN
INSERT INTO flavored_things(thing_id, flavor)
VALUES (insert_thing(NEW), NEW.flavor);
RETURN NEW;
ELSIF ...
record is not a valid type outside PL/pgSQL, it's just a generic placeholder for a yet unknown row type in PL/pgSQL) so you cannot use it for an input parameter in a function declaration.
For a more dynamic function accepting various row types you could use a polymorphic type. Examples:
How to return a table by rowtype in PL/pgSQL
Refactor a PL/pgSQL function to return the output of various SELECT queries
How to write a function that returns text or integer values?
Basically you can convert a record to a hstore variable and pass the hstore variable instead of a record variable to a function. You convert record to hstore i.e. so:
DECLARE r record; h hstore;
h = hstore(r);
Your helper function should also be changed so:
CREATE FUNCTION insert_thing (new_thing hstore) RETURNS INTEGER AS $fun$
DECLARE
inserted_id INT;
BEGIN
INSERT INTO things (name) VALUES (
new_thing -> 'name'
-- (plus 30 more columns)
) RETURNING id INTO inserted_id;
RETURN inserted_id;
END;
$fun$ LANGUAGE plpgsql;
And the call:
inserted_id = insert_thing(hstore(NEW));
hope it helps
Composite types. PostgresSQL has documentation on this, you essentially need to use something like
'()' or ROW() to construct the composite type for a row to pass into a function.

Calling set-returning function with each element in array

I have a set-returning function (SRF) that accepts an integer argument and returns a set of rows from a table. I call it using SELECT * FROM tst.mySRF(3);, and then manipulate the returned value as if it were a table.
What I would like to do is to execute it on each element of an array; however, when I call it using SELECT * FROM tst.mySRF(unnest(array[3,4]));, an error is returned "set-valued function called in context that cannot accept a set". If I instead call it using SELECT tst.mySRF(unnest(array[3,4]));, I get a set of the type tst.tbl.
Table definition:
DROP TABLE tst.tbl CASCADE;
CREATE TABLE tst.tbl (
id serial NOT NULL,
txt text NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO tst.tbl(txt) VALUES ('a'), ('b'), ('c'), ('d');
Function definition:
CREATE OR REPLACE FUNCTION tst.mySRF(
IN p_id integer
)
RETURNS setof tst.tbl
LANGUAGE plpgsql
AS $body$
DECLARE
BEGIN
RETURN QUERY
SELECT id, txt
FROM tst.tbl
WHERE id = p_id;
END;
$body$;
Calls:
SELECT * FROM tst.mySRF(3) returns a table, as expected.
SELECT tst.mySRF(unnest(array[3,4])) returns a table with a single column of the type tst.tbl, as expected.
SELECT * FROM tst.mySRF(unnest(array[3,4])) returns the error described above, I had expected a table.
To avoid the "table of single column" problem, you need to explicitly expand the SRF results with the (row).* notation
SELECT (tst.mySRF(unnest(array[3,4]))).*;
If I understood #depesz, LATERAL will provide a more efficient or straightforward way to achieve the same result.

Call stored proc from after insert trigger

Perhaps a stupid question!
If I call a stored proc from an After Insert trigger (T-SQL) - then how do I get the values of the "just inserted" data?
e.g.
CREATE TRIGGER dbo.MyTrigger
ON dbo.MyTable
AFTER INSERT
AS
BEGIN
EXEC createAuditSproc 'I NEED VALUES HERE!'
I don't have any identity columns to worry about - I just want to use some of the "just inserted" values to pass into my sproc.
Edit: For clarification - I need this to call a sproc and not do a direct insert to the table, since the sproc does more than one thing. I'm working with some legacy tables I can't currently amend to do things 'properly' (time/resource/legacy code), so I have to work with what I have :(
You get to the newly 'changed' data by using the INSERTED and DELETED pseudo-tables:
CREATE TRIGGER dbo.MyTrigger
ON dbo.MyTable
AFTER INSERT
AS
BEGIN
INSERT INTO myTableAudit(ID, Name)
SELECT i.ID, i.Name
FROM inserted i;
END
Given the example tables
create table myTable
(
ID INT identity(1,1),
Name varchar(10)
)
GO
create table myTableAudit
(
ID INT,
Name varchar(10),
TimeChanged datetime default CURRENT_TIMESTAMP
)
GO
Edit : Apologies, I didn't address the bit about calling a Stored Proc. As per marc_s's comment, note that inserted / deleted can contain multiple rows, which complicates matters with a SPROC. Personally, I would leave the trigger inserting directly into the audit table without the encapsulation of a SPROC. However, if you have SQL 2008, you can use table valued parameters, like so:
CREATE TYPE MyTableType AS TABLE
(
ID INT,
Name varchar(10)
);
GO
CREATE PROC dbo.MyAuditProc #MyTableTypeTVP MyTableType READONLY
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO myTableAudit(ID, Name)
SELECT mtt.ID, mtt.Name
FROM #MyTableTypeTVP mtt;
END
GO
And then your trigger would be altered as like so:
ALTER TRIGGER dbo.MyTrigger
ON dbo.MyTable
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #MyTableTypeTVP AS MyTableType;
INSERT INTO #MyTableTypeTVP(ID, Name)
SELECT i.ID, i.Name
FROM inserted i;
EXEC dbo.MyAuditProc #MyTableTypeTVP;
END
you can then test that this works for both a single and multiple inserts
insert into dbo.MyTable values ('single');
insert into dbo.MyTable
select 'double'
union
select 'insert';
However, if you are using SQL 2005 or lower, you would probably need to use a cursor to loop through inserted passing rows to your SPROC, something too horrible to contemplate.
As a side note, if you have SQL 2008, you might look at Change Data Capture
Edit #2 : Since you need to call the proc, and if you are certain that you only insert one row ...
ALTER TRIGGER dbo.MyTrigger
ON dbo.MyTable
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SomeInt INT;
DECLARE #SomeName VARCHAR(10);
SELECT TOP 1 #SomeInt = i.ID, #SomeName = i.Name
FROM INSERTED i;
EXEC dbo.MyAuditProc #SomeInt, #SomeName;
END;