How to turn SQL script into a function? - oracle-sqldeveloper

So I have an SQL script (written in SQL Developer) that I'm using to create some state-specific data. I have to run this script for every state in the US. Right now there are several places in my script where I have a clause restricting the output to a given state (e.g., "where [table.column] = 'AK' "). So, if I want to run the script for different states, I have to manually replace every instance of the given state code (e.g., from 'AK' to 'AL').
Is there a way to turn my SQL script into a function, with the state code as the function's single parameter? It would be really nice if I could just type in "function([state code])" instead of the scrolling through the whole script and tweaking it 50 times!

Hope this helps:
Create sample table/data:
create table us_census
(state varchar2(10),
county_population number,
county varchar2(10)
);
insert into us_census VALUES ('AK',100,'county1');
insert into us_census VALUES ('AK',150,'county2');
insert into us_census VALUES ('AL',200,'county3');
insert into us_census VALUES ('AL',100,'county4');
Sample function:
CREATE FUNCTION get_population_per_state(p_state IN varchar2)
RETURN NUMBER
IS population NUMBER;
BEGIN
SELECT sum(county_population)
INTO population
FROM us_census
WHERE state = p_state --for example: 'AK'
GROUP BY state;
RETURN(population);
END;
/
sample execution:
declare v_population number;
BEGIN
for r in (select distinct state from us_census)
loop
select get_population_per_state(r.state) into v_population from dual;
DBMS_OUTPUT.PUT_LINE('State: ' || r.state || ' Population :' ||v_population);
end loop;
END;
Sample Result:
State: AK Population :250
State: AL Population :300

Related

String replacement in Postgresql originating an array of additional strings

Suppose you have two tables with substitutions which MUST be kept as they are and another table containing a body of names. How could I get all the possible substitutions?
Substitution Table
--------------------------------------
word subs_list
MOUNTAIN MOUNTAIN, MOUNT, MT, MTN
HOUSE HAUS, HOUSE
VIEW VU, VIEW
Synonyms table
-------------------------------------------------
EDUCATION SCHOOL, UNIVERSITY, COLLEGE, TRAINING
FOOD STORE, FOOD, CAFE
STORE FOOD, STORE, MARKET
REFRIGERATION FOODLOCKER, FREEZE, FRIDGE
names table
------------------------------------------------
MOUNT VU FOOD USA
MOUNTAIN VU STORE CA
Note: I know that it would be desirable to have just one substitution table, but both substution tables must remain because they served to additional purposes than the one explained above, those tables are already in used. In addition, the list of replacements in both tables are just a varchar with a string separated by commas
Considering the previous, the problem is to generate possible names derived by substitution. For instance, the name MOUNT VU FOOD USA should be decomposed to MOUNTAIN VIEW FOOD USA and MOUNTAIN VIEW STORE USA, the same fashion would apply for the second.
I have been able to get the replacements in a wrong order and all together in function, there is a way
to get an array as output with the different names generated after replacement? So far I have created this function for replacement:
create or replace function replace_companies_array(i_sentence IN VARCHAR) returns VARCHAR[] AS $p_replaced$
DECLARE
p_replaced VARCHAR[];
subs RECORD;
flag boolean:= True;
cur_s CURSOR(i_sentence VARCHAR)
FOR SELECT w.input, coalesce(x.word, w.input) as word, count(*) OVER (PARTITION BY w.input) as counter
FROM regexp_split_to_table(trim(i_sentence), '\s') as w(input)
LEFT JOIN (
select s.word, trim(s1.token) as token
from subs01 s
cross join unnest(string_to_array(s.subs_list, ',')) s1(token)
union
select sy.word, trim(s2.token) as token
from syns01 sy
cross join unnest(string_to_array(sy.syn_list, ',')) s2(token)
) as x on lower(trim(w.input)) = lower(x.token)
order by counter;
BEGIN
OPEN cur_s(i_sentence);
LOOP
--fetch row into the substitutions
FETCH cur_s INTO subs;
--Exit when no more rows to fetch
EXIT WHEN NOT FOUND;
SELECT REGEXP_REPLACE(i_sentence,'(^|[^a-z0-9])' || subs.input || '($|[^a-z0-9])','\1' || UPPER(subs.word) || '\2','g')
INTO i_sentence;
END LOOP;
p_replaced:=array_append(p_replaced, i_sentence);
RETURN p_replaced;
END;
$p_replaced$ LANGUAGE plpgsql;
Thank you so much for your contributions
I didn't manage to get the final result, but I'w quite close to it!
From sentence: MOUNT VU FOOD USA, I obtain {"MOUNTAIN VIEW MARKET USA","MOUNTAIN VIEW STORE USA","MOUNTAIN VIEW CAFE USA","MOUNTAIN VIEW FOOD USA"}
Here are all my script to recreate the synonyms & substitute tables:
DROP TABLE IF EXISTS subs01;
DROP TABLE IF EXISTS syns01;
CREATE TABLE subs01 (word VARCHAR(20), subs_list VARCHAR(200));
CREATE TABLE syns01 (word VARCHAR(20), syn_list VARCHAR(200));
INSERT INTO subs01 (word, subs_list) VALUES ('MOUNTAIN', 'MOUNTAIN, MOUNT, MT, MTN'),('HOUSE', 'HAUS, HOUSE'),('VIEW', 'VU, VIEW');
INSERT INTO syns01 (word, syn_list) VALUES ('EDUCATION', 'SCHOOL, UNIVERSITY, COLLEGE, TRAINING'),('FOOD', 'STORE, FOOD, CAFE'),('STORE', 'FOOD, STORE, MARKET'),('REFRIGERATION', 'FOODLOCKER, FREEZE, FRIDGE');
I decided to split the job into 2 phases:
Substitute the words:
CREATE OR REPLACE function substitute_words (i_sentence IN VARCHAR) returns VARCHAR AS $p_substituted$
DECLARE
--p_substituted VARCHAR;
subs_cursor CURSOR FOR select su.word, trim(s2.token) as token from subs01 su cross join unnest(string_to_array(su.subs_list, ',')) s2(token);
subs_record record;
BEGIN
OPEN subs_cursor;
LOOP
FETCH subs_cursor INTO subs_record;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'INFO : TOKEN (%) ',subs_record.token ;
IF i_sentence LIKE '%'|| subs_record.token || '%' THEN
RAISE NOTICE '-- FOUND : TOKEN (%) ',subs_record.token ;
SELECT replace (i_sentence, subs_record.token, subs_record.word) INTO i_sentence;
END IF;
END LOOP;
CLOSE subs_cursor;
RETURN i_sentence;
END
$p_substituted$ LANGUAGE plpgsql;
Replace known words by their synomyms:
CREATE OR REPLACE function synonymize_sentence (i_sentence IN VARCHAR) returns TABLE (sentence_result VARCHAR) AS $p_syn$
DECLARE
syn_cursor CURSOR FOR select su.word, trim(s2.token) as token from syns01 su cross join unnest(string_to_array(su.syn_list, ',')) s2(token);
syn_record record;
BEGIN
CREATE TEMPORARY TABLE record_syn (result VARCHAR(200)) ON COMMIT DROP;
INSERT INTO record_syn (result) SELECT i_sentence;
OPEN syn_cursor;
LOOP
FETCH syn_cursor INTO syn_record;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'INFO : WORD (%) ',syn_record.word ;
INSERT INTO record_syn (result) SELECT replace (result, syn_record.word, syn_record.token) FROM record_syn where result LIKE '%'|| syn_record.word || '%';
END LOOP;
CLOSE syn_cursor;
RETURN QUERY SELECT distinct result FROM record_syn;
END;
$p_syn$ LANGUAGE plpgsql;
Then, to generate the result array, I perform this statement:
SELECT ARRAY(SELECT synonymize_sentence (substitute_words ('MOUNT VU FOOD USA')));

how to use a loop with the function pgr_dijkstra

I want to use a loop to calculate the distance traveled between two nodes 152 and 17720 (I use the function pgr_dijkstra) by deleting each time a cell. A cell contains several road links. the grid_edges_routard table contains the road links and the corresponding cell.
Iwant to have for each blocked cell the distance traveled between the two nodes.
I must use pgr_dijkstra to display in a second time the links traveled.
CREATE OR REPLACE FUNCTION get_dist_grid()
RETURNS TABLE (
celref_blocked INT,
dist INT
) AS $$
DECLARE
var_r record;
BEGIN
FOR var_r IN(SELECT distinct(cellule)as cel from grid_edges_routard )
LOOP
SELECT * FROM pgr_dijkstra('SELECT id, source, target,cost
FROM road_routard.edges_vulnerabilite
where id not in (select edge_id
from grid_edges_routard
where cellule=var_r) ',152 ,17720, FALSE)
where edge=-1;
celref_blocked := var_r.cel ;
RETURN NEXT;
END LOOP;
END; $$
LANGUAGE 'plpgsql';
select get_dist_grid()
I have an error message: ERROR: column « var_r » does not exist.
I use postgresql 9.5.
Define a new variable (var_q) of type record, then Execute your select query into your defined variable like this Execute 'SELECT * FROM pgr_dijkstra(''SELECT id, source, target,cost FROM road_routard.edges_vulnerabilite where id not in (select edge_id from grid_edges_routard where cellule='||var_r||') '',152 ,17720, FALSE) where edge=-1' into var_q
This might give some errors as we have to escape the quotes for inner query, Try escaping quotes if it doesn't work and then you can use the out of the query in similar way as you have used celref_blocked := var_r.cel

Postgres Macro, Concat Interval Days and Table name to use in Query

I need to create a macro or function that takes in two parameters, and concats them together after a basic data manipulation. Then this text string should be able to be used in any other query.
CREATE OR REPLACE FUNCTION getData (_table text, _days text)
RETURNS text AS
$func$
SELECT $1 || to_char(CURRENT_TIMESTAMP - ($2 || ' days')::INTERVAL, 'YYYYMMDD');
$func$ LANGUAGE sql;
select * from getData('opportunity', '4') limit 10;
So what I am expecting from this is to actually get the same result as if I executed
select * from opportunity20151030 limit 10;
Instead I am getting "opportunity20151030"
EDIT:
The reason we need this is because my employer is doing a nightly snapshot of our salesforce data, about 17 objects in all. Thats why I can't return a table. Returning a table needs to specify the columns. But we need to be able to query a variety of tables. This really needs to be just a small utility macro. This way we can have one query, to generate graphs that compare data from today and a week ago. This can even be something inside of pgAdmin itself, and is not restricted to being postgresql function. Is there a way I can execute the function and use the result inline in another query. I spent an hour playing around with
Execute 'select * from $1' using getData('opportunity', '4')
type queries, but apparently the LANGUAGE specified changes what can and can't be used in terms of compatible SQL statements.
Thank You!
Well, your function is returning exactly what you asked for... the name of the table you want to look in.
Now, while you can do a function that returns some table's data, the function must know the "structure" of that data. Which means that you can't use it for any table but those that share the same structure (same number of fields, same data types in the same order). Guessing about your table's name i will say that is an inherited table and a function like the one belowe can do the trick for any derived (inherited) table from opportunity
CREATE OR REPLACE FUNCTION getData (_table text, _days text, _limit int default -1)
RETURNS SETOF opportunity AS
$func$
DECLARE
table_name text;
limit_clause text = ' ';
BEGIN
SELECT _table || to_char(CURRENT_TIMESTAMP - (_days || ' days')::INTERVAL, 'YYYYMMDD') INTO table_name;
IF _limit > -1 THEN
limit_clause = ' LIMIT ' || _limit;
END IF;
RETURN QUERY EXECUTE 'SELECT * FROM ' || table_name || limit_clause;
RETURN;
END
$func$ LANGUAGE plpgsql;
And use it like:
select * from getData('opportunity', '4', 10);
PS1: i put the limit as one extra parameter in the function, but because that parameter has a default value you can ignore it when calling the function and that will return all values. i put it there because otherwise the function will return all rows in the table and limit after that.
PS2: i would avoid doing this unless you really think is needed, because this cannot be optimized if put as part of a larger query.

how to call postgresql stored procs from inside another stored proc and include return values in queries

I have a postgresql function / stored proc that does the following:
1. calls another function and saves the value into a variable.
2. executes another sql statement using the value I got from step one as an argument.
My problem is that the query is not returning any data. No errors are returned either.
I'm just new to postgresql so I don't know the best way to debug... but I added a RAISE NOTICE command right after step 1, like so:
SELECT INTO active_id get_widget_id(widget_desc);
RAISE NOTICE 'Active ID is:(%)', active_id;
In the "Messages" section of the pgadmin3 screen, I see the debug message with the data:
NOTICE: Active ID is:(2)
I'm wondering whether or not the brackets are causing the problem for me.
Here's the sql I'm trying to run in step 2:
SELECT d.id, d.contact_id, d.priority, cp.contact
FROM widget_details d, contact_profile cp, contact_type ct
WHERE d.rule_id=active_id
AND d.active_yn = 't'
AND cp.id=d.contact_id
AND cp.contact_type_id=ct.id
AND ct.name = 'email'
Order by d.priority ASC
You'll notice that in my where clause I am referencing the variable "active_id".
I know that this query should return at least one row because when i run a straight sql select (vs using this function) and substitute the value 2 for the variable "active_id", I get back the data I'm looking for.
Any suggetions would be appreciated.
Thanks.
EDIT 1:
Here's the full function definition:
CREATE TYPE custom_return_type AS (
widgetnum integer,
contactid integer,
priority integer,
contactdetails character varying
);
CREATE OR REPLACE FUNCTION test(widget_desc integer)
RETURNS SETOF custom_return_type AS
$BODY$
DECLARE
active_id integer;
rec custom_return_type ;
BEGIN
SELECT INTO active_id get_widget_id(widget_desc);
RAISE NOTICE 'Active ID is:(%)', active_id;
FOR rec IN
SELECT d.id, d.contact_id, d.priority, cp.contact
FROM widget_details d, contact_profile cp, contact_type ct
WHERE d.rule_id=active_id
AND d.active_yn = 't'
AND cp.id=d.contact_id
AND cp.contact_type_id=ct.id
AND ct.name = 'email'
Order by d.priority ASC
LOOP
RETURN NEXT rec;
END LOOP;
END
$BODY$
That's several levels of too-complicated (edit: as it turns out that Erwin already explained to you last time you posted the same thing). Start by using RETURNS TABLE and RETURN QUERY:
CREATE OR REPLACE FUNCTION test(fmfm_number integer)
RETURNS TABLE (
widgetnum integer,
contactid integer,
priority integer,
contactdetails character varying
) AS
$BODY$
BEGIN
RETURN QUERY SELECT d.id, d.contact_id, d.priority, cp.contact
FROM widget_details d, contact_profile cp, contact_type ct
WHERE d.rule_id = get_widget_id(widget_desc)
AND d.active_yn = 't'
AND cp.id=d.contact_id
AND cp.contact_type_id=ct.id
AND ct.name = 'email'
Order by d.priority ASC;
END
$BODY$ LANGUAGE plpgsql;
at which point it's probably simple enough to be turned into a trivial SQL function or even a view. Hard to be sure, since the function doesn't make tons of sense as written:
You never use the parameter fmfm_number anywhere; and
widget_desc is never defined
so this function could never run. Clearly you haven't shown us the real source code, but some kind of "simplified" code that doesn't match the code you're really having issues with.
There is a difference between:
SELECT INTO ...
[http://www.postgresql.org/docs/current/interactive/sql-selectinto.html]
and
SELECT select_expressions INTO [STRICT] target FROM ...;
[http://www.postgresql.org/docs/current/interactive/plpgsql-statements.html#PLPGSQL-STATEMENTS-SQL-ONEROW]
I think you want:
SELECT get_widget_id(widget_desc) INTO active_id;

Using the now() function and executing triggers

I am trying to create a trigger function in PostgreSQL that should check records with the same id (i.e. comparison by id with existing records) before inserting or updating the records. If the function finds records that have the same id, then that entry is set to be the time_dead. Let me explain with this example:
INSERT INTO persons (id, time_create, time_dead, name)
VALUES (1, 'now();', ' ', 'james');
I want to have a table like this:
id time_create time-dead name
1 06:12 henry
2 07:12 muka
id 1 had a time_create 06.12 but the time_dead was NULL. This is the same as id 2 but next time I try to run the insert query with same id but different names I should get a table like this:
id time_create time-dead name
1 06:12 14:35 henry
2 07:12 muka
1 14:35 waks
henry and waks share the same id 1. After running an insert query henry's time_dead is equal to waks' time_create. If another entry was to made with id 1, lets say for james, the time entry for james will be equal to the time_dead for waks. And so on.
So far my function looks like this. But it's not working:
CREATE FUNCTION tr_function() RETURNS trigger AS '
BEGIN
IF tg_op = ''UPDATE'' THEN
UPDATE persons
SET time_dead = NEW.time_create
Where
id = NEW.id
AND time_dead IS NULL
;
END IF;
RETURN new;
END
' LANGUAGE plpgsql;
CREATE TRIGGER sofgr BEFORE INSERT OR UPDATE
ON persons FOR each ROW
EXECUTE PROCEDURE tr_function();
When I run this its say time_dead is not supposed to be null. Is there a way I can write a trigger function that will automatically enter the time upon inserting or updating but give me results like the above tables when I run a select query?
What am I doing wrong?
My two tables:
CREATE TABLE temporary_object
(
id integer NOT NULL,
time_create timestamp without time zone NOT NULL,
time_dead timestamp without time zone,
PRIMARY KEY (id, time_create)
);
CREATE TABLE persons
(
name text
)
INHERITS (temporary_object);
Trigger function
CREATE FUNCTION tr_function()
RETURNS trigger AS
$func$
BEGIN
UPDATE persons p
SET time_dead = NEW.time_create
WHERE p.id = NEW.id
AND p.time_dead IS NULL
AND p.name <> NEW.name;
RETURN NEW;
END
$func$ LANGUAGE plpgsql;
You were missing the INSERT case in your trigger function (IF tg_op = ''UPDATE''). But there is no need for checking TG_OP to begin with, since the trigger only fires on INSERT OR UPDATE - assuming you don't use the same function in other triggers. So I removed the cruft.
Note that you don't have to escape single quotes inside a dollar-quoted string.
Also added:
AND p.name <> NEW.name
... to prevent INSERT's from terminating themselves instantly (and causing an infinite recursion). This assumes that a row can never succeed another row with the same name.
Aside: The setup is still not bullet-proof. UPDATEs could mess with your system. I could keep updating the id or a row, thereby terminating other rows but not leaving a successor. Consider disallowing updates on id. Of course, that would make the trigger ON UPDATE pointless. I doubt you need that to begin with.
now() as DEFAULT
If you want to use now() as default for time_create just make it so. Read the manual about setting a column DEFAULT. Then skip time_create in INSERTs and it is filled automatically.
If you want to force it (prevent everyone from entering a different value) create a trigger ON INSERT or add the following at the top of your trigger:
IF TG_OP = 'INSERT' THEN
NEW.time_create := now(); -- type timestamp or timestamptz!
RETURN NEW;
END IF;
Assuming your missleadingly named column "time_create" is actually a timestamp type.
That would force the current timestamp for new rows.