DBMS_SQL Character constraint - oracle-sqldeveloper

I encountered the following error, i'm learning about DBMS_SQL.
I was playing around with the below code which drops a table and recreates a new table. I noticed for the recreation of the new table, i'm running into a character constraint. Just wondering what the best solution is to fix this.
I've tried to concatenating two strings but that didnt work.
'CREATE TABLE students_12345(s_id NUMBER, fname VARCHAR2(30),lname VARCHAR2(30),tname VARCHAR2(100), score NUMBER, exam_result VARCHAR2(6))';
Its probably something really simple but its taken me a couple of hours to get to this point in the code and my eyes are going square shaped at this point.
Thanks in advance!
Error starting at line : 48 in command -
EXECUTE RecreateTempTable('a')
Error report -
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "SYS.RECREATETEMPTABLE", line 33
ORA-06512: at line 1
06502. 00000 - "PL/SQL: numeric or value error%s"
*Cause: An arithmetic, numeric, string, conversion, or constraint error
occurred. For example, this error occurs if an attempt is made to
assign the value NULL to a variable declared NOT NULL, or if an
attempt is made to assign an integer larger than 99 to a variable
declared NUMBER(2).
*Action: Change the data, how it is manipulated, or how it is declared so
that values do not violate constraints.
CREATE TABLE students_12345
(student_id NUMBER,
first_name VARCHAR2(30),
last_name VARCHAR2(30),
test_name VARCHAR2(100),
score NUMBER,
exam_result VARCHAR2(6));
/
-----------------------------------------------------------------------------
CREATE OR REPLACE PROCEDURE RecreateTempTable (
p_description IN VARCHAR2) IS
v_descrip VARCHAR2(100) := p_description;
v_cursor NUMBER;
v_createstring VARCHAR2(100);
v_dropstring VARCHAR2(100);
v_numrows INTEGER;
BEGIN
v_cursor := DBMS_SQL.OPEN_CURSOR;
v_dropstring := 'DROP TABLE students_12345';
BEGIN
-- parse the query using the parameter table name
DBMS_SQL.PARSE(v_cursor, v_dropString, DBMS_SQL.NATIVE);
-- execute the cursor query
v_numrows := DBMS_SQL.EXECUTE(v_cursor);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -942 THEN
RAISE;
END IF;
END;
v_createstring := 'CREATE TABLE students_12345(s_id NUMBER, fname VARCHAR2(30),lname VARCHAR2(30),tname VARCHAR2(100), score NUMBER)';
DBMS_SQL.PARSE(v_cursor, v_createstring, DBMS_SQL.NATIVE);
v_numrows := DBMS_SQL.EXECUTE(v_cursor);
DBMS_SQL.CLOSE_CURSOR(v_cursor);
EXCEPTION
WHEN OTHERS THEN
DBMS_SQL.CLOSE_CURSOR(v_cursor);
RAISE;
END RecreateTempTable;
/
-------------------------------------------------------------------------------
EXECUTE RecreateTempTable('a');

Related

Problem with PLPGSQL function return, uuid involved

I want to assign serial numbers from a sequence, along with uuids that are generated automatically. My table looks like
CREATE TABLE master_serialnumbers
(
uuident uuid PRIMARY KEY DEFAULT uuid_generate_v1(),
serno integer GENERATED ALWAYS AS IDENTITY UNIQUE,
requester varchar(20) NOT NULL
);
I can now generate a serial number with
INSERT INTO master_serialnumbers(requester) VALUES ('XY')
RETURNING (uuident,serno,requester);
To automate this and generate a block of several serial numbers, I can use a function like this:
CREATE FUNCTION gen_serials_A(num_serials integer, req varchar(20))
RETURNS SETOF master_serialnumbers
AS $$
DECLARE
new_id uuid;
rr master_serialnumbers%ROWTYPE; -- or simply RECORD
tmpSQL TEXT;
BEGIN
RAISE NOTICE 'generate % serial numbers', num_serials;
CREATE TEMPORARY TABLE tmpIds (id uuid NOT NULL PRIMARY KEY);
FOR i IN 1..num_serials LOOP
RAISE NOTICE 'serno %', i;
INSERT INTO master_serialnumbers(space, requester) VALUES (spc, req)
RETURNING (uuident) INTO new_id;
INSERT INTO tmpIds(id) VALUES (new_id);
END LOOP;
FOR rr IN EXECUTE 'SELECT * FROM master_serialnumbers
WHERE uuident IN (SELECT id FROM tmpIds)' LOOP
RETURN NEXT rr;
END LOOP;
tmpSQL := 'DROP TABLE tmpIds';
EXECUTE tmpSQL;
RETURN;
END
$$ LANGUAGE plpgsql;
This does work, but it seems cumbersome to store the values in a temporary table and have two loops. So I tried to shorten it like this:
CREATE FUNCTION gen_serials(num_serials integer, req varchar(20))
RETURNS SETOF master_serialnumbers
AS $$
DECLARE
rr master_serialnumbers%ROWTYPE;
BEGIN
RAISE NOTICE 'generate % serial numbers', num_serials;
FOR i IN 1..num_serials LOOP
RAISE NOTICE 'serno %', i;
INSERT INTO master_serialnumbers(requester) VALUES (req)
RETURNING (uuident,serno,requester) INTO rr;
RAISE NOTICE 'rr is %', rr;
RETURN NEXT rr;
END LOOP;
RETURN;
END
$$ LANGUAGE plpgsql;
– but the INSERT throws an error
invalid input syntax for type uuid: »(12345678-1234-1234-1234-123456789abc,1,A)«
Changing the declaration to rr RECORD now passes the INSERT:
rr is ("(12345678-1234-1234-1234-123456789abc,1,A)")
ERROR: wrong record type supplied in RETURN NEXT
DETAIL: Returned type does not match expected type uuid in column 1.
Apparently all has been converted into a string.
And changing the return type also to SETOF RECORD yields another error:
ERROR: function with set result called in a context that cannot process set results
(German error messages translated back to English by me, sorry for that).
Any ideas what's (not) happening here? Perhaps I am doing it much too long-winded at all by using a function with procedural loops?
To automate this and generate a block of several serial numbers, I can use a function
Don't make it so complicated. Use something like
INSERT INTO master_serialnumbers(requester)
VALUES ('XY', 'YZ', 'ZA')
RETURNING uuident, serno, requester;
or
INSERT INTO master_serialnumbers(requester)
SELECT UNNEST(ARRAY['XY', 'YZ', 'ZA']) -- useful for parameterised queries
RETURNING uuident, serno, requester;
or
INSERT INTO master_serialnumbers(requester)
SELECT 'XY'
FROM generate_series(1, 20) -- useful for arbitrary repetition of the same value
RETURNING uuident, serno, requester;
(a set-returning function to create a series of numbers)

Error key already exists appears only while executing function

I have 2 tables, in one I create an element, in the other I save the details of said element in different languages.
The first table:
CREATE TABLE public.element
(
idelement integer NOT NULL DEFAULT nextval('element_idelement_seq'::regclass),
image_location text,
price numeric(6,2) NOT NULL DEFAULT 0,
CONSTRAINT element_pkey PRIMARY KEY (idelement)
)
the second table:
CREATE TABLE public.elementdetails
(
idelement integer NOT NULL,
title text,
description text,
notes text,
language text NOT NULL,
CONSTRAINT elementdetails_pkey PRIMARY KEY (idelement, language)
)
I use a function to insert the new elements in plpgsql
IF _idelement = 0 THEN
INSERT INTO element (price) VALUES (0.0);
SELECT lastval() INTO _idelement;
EXECUTE FORMAT('INSERT INTO elementdetails(idelement, %I, language) VALUES (%L, %L, %L))', _fieldname, _idelement, _value, _language);
END IF;
But it raises an error saying the key constraints (idelement, language) already exists.
Simply copying the SQL appearing in the error message and executing it in another window would work with no issues and the problem appears only while within the pgsql function.
EDIT: I'll add that this worked until I noticed an issue where I let people insert the language iso both in uppercase or lowercase, so I forced the language to always be uppercase, since then I get the error message.
I found the problem:
right after that statement I had another one
IF _idelement > 0 THEN
EXECUTE FORMAT('INSERT INTO elementdetails(idelement, %I, language) VALUES (%L, %L, %L))', _fieldname, _idelement, _value, _language);
END IF;
but I forgot to put it after an ELSE so both INSERTs were executed which raised the error.

transform character varying [] to character varying postgresql

I have multiple input files that I want to run through the same SQL file. Sometimes a column that I run a regular expression on is an character varying[] and sometimes that same column in a different file is a character varying without the []. I want to make sure that it does not matter whether the input file is a character varying with or without the [].
The following works for an individual file:
ALTER TABLE table ALTER COLUMN column TYPE character varying;
UPDATE table SET column = left(column , length(column )-1);
UPDATE table SET column = right(column , length(column )-1);
When changing the type from character varying[] to character varying it keeps the brackets that I have to remove with an update statement. I am not managing to create a solution that works for both input files.
I think this direction of thought might be the answer but PL SQL is difficult for me: http://www.postgresqltutorial.com/plpgsql-if-else-statements/ . Working on this at the moment but not sure if I am heading in the right direction:
DO $$
DECLARE
??? a integer := 10;
??? b integer := 10;
BEGIN
IF column === character varying[] THEN
ALTER TABLE table ALTER COLUMN column TYPE character varying;
UPDATE table SET column = left(column , length(column )-1);
UPDATE table SET column = right(column , length(column )-1);
ELSE
RAISE NOTICE 'column is a character varying';
END IF;
END $$;
Fixed it by doing the following:
ALTER TABLE elektriciteitskabel ALTER COLUMN link_href TYPE character varying;
UPDATE elektriciteitskabel SET link_href = replace(link_href, '{', '');
UPDATE elektriciteitskabel SET link_href = replace(link_href, '}', '');

How to use variable settings in trigger functions?

I would like to record the id of a user in the session/transaction, using SET, so I could be able to access it later in a trigger function, using current_setting. Basically, I'm trying option n2 from a very similar ticket posted previously, with the difference that I'm using PG 10.1 .
I've been trying 3 approaches to setting the variable:
SET local myvars.user_id = 4, thereby setting it locally in the transaction;
SET myvars.user_id = 4, thereby setting it in the session;
SELECT set_config('myvars.user_id', '4', false), which depending of the last argument, will be a shortcut for the previous 2 options.
None of them is usable in the trigger, which receives NULL when getting the variable through current_setting. Here is a script I've devised to troubleshoot it (can be easily used with the postgres docker image):
database=$POSTGRES_DB
user=$POSTGRES_USER
[ -z "$user" ] && user="postgres"
psql -v ON_ERROR_STOP=1 --username "$user" $database <<-EOSQL
DROP TRIGGER IF EXISTS add_transition1 ON houses;
CREATE TABLE IF NOT EXISTS houses (
id SERIAL NOT NULL,
name VARCHAR(80),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id)
);
CREATE TABLE IF NOT EXISTS transitions1 (
id SERIAL NOT NULL,
house_id INTEGER,
user_id INTEGER,
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now(),
PRIMARY KEY(id),
FOREIGN KEY(house_id) REFERENCES houses (id) ON DELETE CASCADE
);
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS \$\$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer || NULL;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
\$\$ LANGUAGE plpgsql;
CREATE TRIGGER add_transition1 AFTER INSERT OR UPDATE ON houses FOR EACH ROW EXECUTE PROCEDURE add_transition1();
BEGIN;
%1% SELECT current_setting('myvars.user_id');
%2% SELECT set_config('myvars.user_id', '55', false);
%3% SELECT current_setting('myvars.user_id');
INSERT INTO houses (name) VALUES ('HOUSE PARTY') RETURNING houses.id;
SELECT * from houses;
SELECT * from transitions1;
COMMIT;
DROP TRIGGER IF EXISTS add_transition1 ON houses;
DROP FUNCTION IF EXISTS add_transition1;
DROP TABLE transitions1;
DROP TABLE houses;
EOSQL
The conclusion I came to was that the function is triggered in a different transaction and a different (?) session. Is this something that one can configure, so that all happens within the same context?
Handle all possible cases for the customized option properly:
option not set yet
All references to it raise an exception, including current_setting() unless called with the second parameter missing_ok. The manual:
If there is no setting named setting_name, current_setting throws an error unless missing_ok is supplied and is true.
option set to a valid integer literal
option set to an invalid integer literal
option reset (which burns down to a special case of 3.)
For instance, if you set a customized option with SET LOCAL or set_config('myvars.user_id3', '55', true), the option value is reset at the end of the transaction. It still exists, can be referenced, but it returns an empty string now ('') - which cannot be cast to integer.
Obvious mistakes in your demo aside, you need to prepare for all 4 cases. So:
CREATE OR REPLACE FUNCTION add_transition1()
RETURNS trigger AS
$func$
DECLARE
_user_id text := current_setting('myvars.user_id', true); -- see 1.
BEGIN
IF _user_id ~ '^\d+$' THEN -- one or more digits?
INSERT INTO transitions1 (user_id, house_id)
VALUES (_user_id::int, NEW.id); -- valid int, cast is safe
ELSE
INSERT INTO transitions1 (user_id, house_id)
VALUES (NULL, NEW.id); -- use NULL instead
RAISE WARNING 'Invalid user_id % for house_id % was reset to NULL!'
, quote_literal(_user_id), NEW.id; -- optional
END IF;
RETURN NULL; -- OK for AFTER trigger
END
$func$ LANGUAGE plpgsql;
db<>fiddle here
Notes:
Avoid variable names that match column names. Very error prone. One popular naming convention is to prepend variable names with an underscore: _user_id.
Assign at declaration time to save one assignment. Note the data type text. We'll cast later, after sorting out invalid input.
Avoid raising / trapping an exception if possible. The manual:
A block containing an EXCEPTION clause is significantly more expensive
to enter and exit than a block without one. Therefore, don't use
EXCEPTION without need.
Test for valid integer strings. This simple regular expression allows only digits (no leading sign, no white space): _user_id ~ '^\d+$'. I reset to NULL for any invalid input. Adapt to your needs.
I added an optional WARNING for your debugging convenience.
Cases 3. and 4. only arise because customized options are string literals (type text), valid data types cannot be enforced automatically.
Related:
User defined variables in PostgreSQL
Is there a way to define a named constant in a PostgreSQL query?
All that aside, there may be more elegant solutions for what you are trying to do without customized options, depending on your exact requirements. Maybe this:
Fastest way to get current user's OID in Postgres?
It is not clear why you are trying to concat NULL to user_id but it is obviously the cause of the problem. Get rid of it:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
user_id := current_setting('myvars.user_id')::integer;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Note that
SELECT 55 || NULL
always gives NULL.
You can catch the exception when the value doesn't exist - here's the changes I made to get this to work:
CREATE OR REPLACE FUNCTION add_transition1() RETURNS TRIGGER AS $$
DECLARE
user_id integer;
BEGIN
BEGIN
user_id := current_setting('myvars.user_id')::integer;
EXCEPTION WHEN OTHERS THEN
user_id := 0;
END;
INSERT INTO transitions1 (user_id, house_id) VALUES (user_id, NEW.id);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION insert_house() RETURNS void as $$
DECLARE
user_id integer;
BEGIN
PERFORM set_config('myvars.user_id', '55', false);
INSERT INTO houses (name) VALUES ('HOUSE PARTY');
END; $$ LANGUAGE plpgsql;

Trying to use CAST on a string to varchar after SET in UPDATE statement - Postgres

I'm trying to cast a string to a varchar from user input on a web application. I want to change the value of one of the columns given a certain ID (Primary Key) and the column name is what I'm casting as the varchar.
CREATE OR REPLACE FUNCTION changeQuantities(productID varchar, warehouseID int, change int)
RETURNS void AS $$
BEGIN
EXECUTE format('UPDATE warehouses SET CAST(%I AS VARCHAR) = %s WHERE warehouseID = %s', productID, change, warehouseID)
USING change, warehouseID;
END;
$$ LANGUAGE plpgsql;
The 'productID' is the column name, 'change' is the new value, and 'warehouseID' is the primary key for the table. 'warehouses' is the table. Here is the error I receive:
SELECT changeQuantities('bg412',1,100);
ERROR: syntax error at or near "CAST"
LINE 1: UPDATE warehouses SET CAST(bg412 AS VARCHAR) = 100 WHERE war...
^
QUERY: UPDATE warehouses SET CAST(bg412 AS VARCHAR) = 100 WHERE warehouseID = 1
CONTEXT: PL/pgSQL function changequantities(character varying,integer,integer) line 3 at EXECUTE statement
I have another function just like it that uses a SELECT statement while casting the column name and it works just fine. Can I just not cast something after SET? I haven't found anything on this particular case, so I'm either going to be humiliated or I will help someone else out with similar issues. Thanks for any help.
You can't have a cast() on the left side of the assignment - and you don't need it, as the data type of a column is known. If at all you would need to cast the right hand side of an assignment to the data type of the left hand side.
Assuming that bg412 is a column name, you need:
CREATE OR REPLACE FUNCTION changeQuantities(productID varchar, warehouseID int, change int)
RETURNS void AS $$
BEGIN
EXECUTE format('UPDATE warehouses SET %I = %s WHERE warehouseID = %s', productID, change, warehouseID)
USING change, warehouseID;
END;
$$ LANGUAGE plpgsql;
Unrelated, but: using the the ID of a product as a column name in a table seems like a horrible design. What do you do if you have a million products?