Is there a naming convention for parameters for stored procedures in postgres? I wanted to distinguish my parameters from my column names to make it easier to read, so student_id (column) = s_id(parameter), period (column) = per (parameter name), assignment (column) = asgmt (parameter), ect..
CREATE PROCEDURE
insert_assignment(s_id INT, per INT, cl VARCHAR(50), ln VARCHAR(50), sp_id INT, asgmt VARCHAR(500))
LANGUAGE SQL
AS $$
INSERT INTO
assignment(sped_id, assignment)
VALUES
(sp_id, asgmt);
UPDATE assignment ss
SET student_schedule_id=
(SELECT
ss.student_schedule_id
FROM
student_schedule ss
LEFT JOIN
class_teacher ct on ct.class_teacher_id=ss.class_teacher_id
LEFT JOIN
teacher t on t.teacher_id=ct.teacher_id
LEFT JOIN
class c on c.class_id=ct.class_id
WHERE
ss.student_id=s_id AND ss.period=per
)
WHERE assignment=asgmt
$$
There is nothing established conventions but there are 2 common themes I seen. But first on either do not truncate parameter names this actually makes reading and understanding them harder; also allign them instead of just stringing them out. The most common is to preference the parameter as p_ . The other is to suffix parameter with its usage an _in or _ot Examples:
CREATE PROCEDURE
insert_assignment(p_student_id INT, p_period INT, p_cl VARCHAR(50), p_ln VARCHAR(50), p_sped_id INT, p_assignment VARCHAR(500))
LANGUAGE SQL ...
or
CREATE PROCEDURE
insert_assignment(student_id_in INT,
period_in INT,
cl_in VARCHAR(50),
ln_in VARCHAR(50),
sped_id_in INT,
assignment_in VARCHAR(500)
)
LANGUAGE SQL ...
Not sure about cl,ln as you did not use them. As for sepd if it is in common use in your organization it is fine otherwise (IMHO) it is a poor name. Make understanding your primary goal, not less typing. (and this from a very poor typist)
Related
i'm pretty new to SQL and struggling with the last piece on my final project. Logically, I know how to do it (i think) I just can't get the syntax correct.
I have a teaches table and an instructor table. The teaches tables tells you what instructor has taught what class, when and for how many credits. The instructor table is obviously a table with IDs and instructor names. Finally, I have a table instructor_taught which really serves no purpose other than for us to learn. this table holds instructor's IDs and how many total credits they have taught.
My challenge is to create a proc that takes an instructor's ID and adds or updates their total credits taught in the instructor_taught table.
First: check if the ID already exists in the instructor_taught table...if it does not exist then I need to add it to the table. If it does exist then I need to update the entry and add the additional 3 credits. so if instructor 101 is already in the table with 3 total credits then I need to update the 3 to 6 instead of creating a new row.
I was able to create a proc that adds the instructor to the instructor_taught table but the update part is failing...so if i enter id:101 10x it will add that instructor 10x instead of updating.
Here is my code:
CREATE OR REPLACE PROCEDURE Day_21_monthlyPayment (IN id VARCHAR(30), INOUT d_count INTEGER DEFAULT 0)
LANGUAGE plpgsql
AS
$$
BEGIN
BEGIN
SELECT teaches.id, instructor.name, COUNT(*) AS d_count
FROM teaches NATURAL JOIN instructor
WHERE teaches.id = Day_21_monthlyPayment.id
GROUP BY teaches.id, instructor.name;
END
IF EXISTS(SELECT teaches.id FROM teaches)
THEN UPDATE instructor_course_nums SET tot_courses = d_count
WHERE teaches.id = Day_21_monthlyPayment.id
ELSE INSERT INTO instructor_course_nums (id, name, tot_courses)
END IF;
END;
$$
I"m pretty sure this is a simple if else statement but i can't get the syntax right. Thanks in advance!
DDL:
CREATE TABLE instructor(
ID VARCHAR(12),
name VARCHAR(30),
section VARCHAR(30),
salary NUMERIC(20)
)
CREATE TABLE teaches(
ID VARCHAR(12),
course VARCHAR(30),
count VARCHAR(30),
term VARCHAR(20),
year NUMERIC(20)
)
CREATE TABLE instructor_course_nums(
ID VARCHAR(12),
name VARCHAR(30),
tot_courses NUMERIC(2)
)
Even if you are forced to do a procedure, I think you can still do this in (nearly) one fell swoop with a single DML statement. Assuming you have the proper constraints (primary key), I would think something like this would work:
CREATE OR REPLACE PROCEDURE Day_21_monthlyPayment (idx VARCHAR(30), INOUT d_count INTEGER DEFAULT 0)
LANGUAGE plpgsql
AS
$BODY$
BEGIN
SELECT COUNT(*)
into d_count
FROM teaches t JOIN instructor i on t.id = i.id
WHERE t.id = idx;
insert into instructor_course_nums
select t.id, t.name, d_count
from instructor t
where t.id = idx
on conflict (id) do
update
set tot_courses = d_count;
END;
$BODY$
I have a table created like
CREATE TABLE data
(value1 smallint references labels,
value2 smallint references labels,
value3 smallint references labels,
otherdata varchar(32)
);
and a second 'label holding' table created like
CREATE TABLE labels (id serial primary key, name varchar(32));
The rationale behind it is that value1-3 are a very limited set of strings (6 options) and it seems inefficient to enter them directly in the data table as varchar types. On the other hand these do occasionally change, which makes enum types unsuitable.
My question is, how can I execute a single query such that instead of the label IDs I get the relevant labels?
I looked at creating a function for it and stumbled at the point where I needed to pass the label holding table name to the function (there are several such (label holding) tables across the schema). Do I need to create a function per label table to avoid that?
create or replace function translate
(ref_id smallint,reference_table regclass) returns varchar(128) as
$$
begin
select name from reference_table where id = ref_id;
return name;
end;
$$
language plpgsql;
And then do
select
translate(value1, labels) as foo,
translate(value2, labels) as bar
from data;
This however errors out with
ERROR: relation "reference_table" does not exist
All suggestions welcome - at this point a can still alter just about anything...
CREATE TABLE labels
( id smallserial primary key
, name varchar(32) UNIQUE -- <<-- might want this, too
);
CREATE TABLE data
( value1 smallint NOT NULL REFERENCES labels(id) -- <<-- here
, value2 smallint NOT NULL REFERENCES labels(id)
, value3 smallint NOT NULL REFERENCES labels(id)
, otherdata varchar(32)
, PRIMARY KEY (value1,value2,value3) -- <<-- added primary key here
);
-- No need for a function here.
-- For small sizes of the `labels` table, the query below will always
-- result in hash-joins to perform the lookups.
SELECT l1.name AS name1, l2.name AS name2, l3.name AS name3
, d.otherdata AS the_data
FROM data d
JOIN labels l1 ON l1.id = d.value1
JOIN labels l2 ON l2.id = d.value2
JOIN labels l3 ON l3.id = d.value3
;
Note: labels.id -> labels.name is a functional dependency (id is the primary key), but that doesn't mean that you need a function. The query just acts like a function.
You can pass the label table name as string, construct a query as string and execute it:
sql = `select name from ` || reference_table_name || `where id = ` || ref_id;
EXECUTE sql INTO name;
RETURN name;
I have a table in postgres:
create table fubar (
name1 text,
name2 text, ...,
key integer);
I want to write a function which returns field values from fubar given the column names:
function getFubarValues(col_name text, key integer) returns text ...
where getFubarValues returns the value of the specified column in the row identified by key. Seems like this should be easy.
I'm at a loss. Can someone help? Thanks.
Klin's answer is a good (i.e. safe) approach to the question as posed, but it can be simplified:
PostgreSQL's -> operator allows expressions. For example:
CREATE TABLE test (
id SERIAL,
js JSON NOT NULL,
k TEXT NOT NULL
);
INSERT INTO test (js,k) VALUES ('{"abc":"def","ghi":"jkl"}','abc');
SELECT js->k AS value FROM test;
Produces
value
-------
"def"
So we can combine that with row_to_json:
CREATE TABLE test (
id SERIAL,
a TEXT,
b TEXT,
k TEXT NOT NULL
);
INSERT INTO test (a,b,k) VALUES
('foo','bar','a'),
('zip','zag','b');
SELECT row_to_json(test)->k AS value FROM test;
Produces:
value
-------
"foo"
"zag"
Here I'm getting the key from the table itself but of course you could get it from any source / expression. It's just a value. Also note that the result returned is a JSON value type (it doesn't know if it's text, numeric, or boolean). If you want it to be text, just cast it: (row_to_json(test)->k)::TEXT
Now that the question itself is answered, here's why you shouldn't do this, and what you should do instead!
Never trust any data. Even if it already lives inside your database, you shouldn't trust it. The method I've posted here is safe against SQL injection attacks, but an attacker could still set k to 'id' and see a column which was not intended to be visible to them.
A much better approach is to structure your data with this type of query in mind. Postgres has some excellent datatypes for this; HSTORE and JSON/JSONB. Merge your dynamic columns into a single column with one of those types (I'd suggest HSTORE for its simplicity and generally being more complete).
This has several advantages: your schema is well-defined and does not need to change if you add more dynamic columns, you do not need to perform expensive re-casting (i.e. row_to_json), and you are able to take advantage of indexes on your columns (thanks to PostgreSQL's functional indexes).
The equivalent to the code I wrote above would be:
CREATE EXTENSION HSTORE; -- necessary if you're not already using HSTORE
CREATE TABLE test (
id SERIAL,
cols HSTORE NOT NULL,
k TEXT NOT NULL
);
INSERT INTO test (cols,k) VALUES
('a=>"foo",b=>"bar"','a'),
('a=>"zip",b=>"zag"','b');
SELECT cols->k AS value FROM test;
Or, for automatic escaping of your values when inserting, you can use one of:
INSERT INTO test (cols,k) VALUES
(hstore( 'a', 'foo' ) || hstore( 'b', 'bar' ), 'a'),
(hstore( ARRAY['a','b'], ARRAY['zip','zag'] ), 'b');
See http://www.postgresql.org/docs/9.1/static/hstore.html for more details.
You can use dynamic SQL to select a column by name:
create or replace function get_fubar_values (col_name text, row_key integer)
returns setof text language plpgsql as $$begin
return query execute 'select ' || quote_ident(col_name) ||
' from fubar where key = $1' using row_key;
end$$;
The end result of what I am after is a query that calls a function and that function returns a set of records that are in their own separate fields. I can do this but the results of the function are all in one field.
ie: http://i.stack.imgur.com/ETLCL.png and the results I am after are: http://i.stack.imgur.com/wqRQ9.png
Here's the code to create the table
CREATE TABLE tbl_1_hm
(
tbl_1_hm_id bigserial NOT NULL,
tbl_1_hm_f1 VARCHAR (250),
tbl_1_hm_f2 INTEGER,
CONSTRAINT tbl_1_hm PRIMARY KEY (tbl_1_hm_id)
)
-- do that for a few times to get some data
INSERT INTO tbl_1_hm (tbl_1_hm_f1, tbl_1_hm_f2)
VALUES ('hello', 1);
CREATE OR REPLACE FUNCTION proc_1_hm(id BIGINT)
RETURNS TABLE(tbl_1_hm_f1 VARCHAR (250), tbl_1_hm_f2 int AS $$
SELECT tbl_1_hm_f1, tbl_1_hm_f2
FROM tbl_1_hm
WHERE tbl_1_hm_id = id
$$ LANGUAGE SQL;
--And here is the current query I am running for my results:
SELECT t1.tbl_1_hm_id, proc_1_hm(t1.tbl_1_hm_id) AS t3
FROM tbl_1_hm AS t1
Thanks for having a read. Please if you want to haggle about the semantics of what I am doing by hitting the same table twice or my naming convention --> this is a simplified test.
When a function returns a set of records, you should treat it as a table source:
SELECT t1.tbl_1_hm_id, t3.*
FROM tbl_1_hm AS t1, proc_1_hm(t1.tbl_1_hm_id) AS t3;
Note that functions are implicitly using a LATERAL join (scroll down to sub-sections 4 and 5) so you can use fields from tables listed previously without having to specify an explicit JOIN condition.
How to make a new TYPE with ENUM that takes all of the members of an interval? Like:
CREATE TYPE letters [a...z]
There is no built-in syntax for this, but you can do it with dynamic SQL:
DO
$$
BEGIN
EXECUTE
(
SELECT 'CREATE TYPE a2z AS ENUM ('''
|| string_agg(chr(ascii('a') + g), ''',''')
|| ''')'
FROM generate_series(0,25) g
);
END
$$;
This builds and executes a statement of the form:
CREATE TYPE a2z AS ENUM ('a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z')
This depends on your locale, but I think that all locales have a-z in a continuous range, which is all that's needed for this. Tested with an UTF-8 locale.
Alternative for anything that won't easily fit into an ENUM
For long lists of values, values that tend to change or are not as simple as the example or for special data types etc. consider creating a small look-up table instead and use a foreign key constraint to it. Example for a selection of dates:
CREATE TABLE my_date (my_date date PRIMARY KEY);
INSERT INTO my_date(my_date) VALUES ('2015-02-03'), ...;
CREATE TABLE foo (
foo_id serial PRIMARY KEY
, my_date date REFERENCES my_date ON UPDATE CASCADE
--, more columns ...
);