Exception handling in PostgreSQL function with Django - django

have write the following store procedure in Postgres. This SP simply accept the incoming parameters, insert it into the table and return current identity. More I have also declare an addition variable that will tell is the sp runs successfully or not.
I am new to Postgres and have not much knowledge about Postgres way to do this. I want some thing like BEGIN TRY, END TRY and BEGIN CATCH, END CATCH like we do in MSSQL.
CREATE OR REPLACE FUNCTION usp_save_message(msg_sub character varying(80), msg_content text, msg_type character(12), msg_category character(255),msg_created_by character(255),msg_updated_by character(255))
RETURNS msg_id character, success boolean AS
$BODY$
DECLARE
msg_id character;
success boolean;
BEGIN
BEGIN TRY:
set success = 0
set msg_id = INSERT INTO tbl_messages(
message_subject, message_content, message_type, message_category,
created_on, created_by, updated_on, updated_by)
VALUES (msg_sub, msg_cont, msg_type,msg_category, LOCALTIMESTAMP,
msg_created_by, LOCALTIMESTAMP, msg_updated_by) RETURNING message_id;
set success = 1
RETURN msg_id,success;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
I want something like this:
begin proc()
BEGIN
BEGIN TRY:
set success = 0
execute the query
set success = 1
END TRY
BEGIN CATCH:
set success = 0
END CATCH
set success = 1
END
More I have to catched both these return values in django views.
I have updated the question and it is as now;
Here is the table,
CREATE TABLE tbl_messages
(
message_subject character varying(80),
message_content text,
message_type character(12),
message_category character(255),
created_on timestamp without time zone,
created_by character(255),
updated_on timestamp without time zone,
updated_by character(255),
message_id serial NOT NULL,
CONSTRAINT tbl_messages_pkey PRIMARY KEY (message_id)
)
WITH (
OIDS=FALSE
);
ALTER TABLE tbl_messages
OWNER TO gljsxdlvpgfvui;
Here is the function i created;
CREATE FUNCTION fn_save_message(IN msg_sub character varying, IN msg_cont text, IN msg_type character varying, IN msg_category character varying, IN msg_created_by character varying, IN msg_updated_by character varying, OUT success boolean, OUT msg_id integer) RETURNS integer AS
$BODY$BEGIN
BEGIN
INSERT INTO tbl_messages
(message_subject, message_content, message_type, message_category,
created_on, created_by, updated_on, updated_by)
VALUES
(msg_sub, msg_cont, msg_type, msg_category, LOCALTIMESTAMP,
msg_created_by, LOCALTIMESTAMP, msg_updated_by)
returning message_id
into msg_id;
success := true;
EXCEPTION
WHEN others then
success := false;
msg_id := null;
END;
return msg_id,success;
END;$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF
COST 100;
ALTER FUNCTION public.fn_save_message(IN character varying, IN text, IN character varying, IN character varying, IN character varying, IN character varying)
OWNER TO gljsxdlvpgfvui;
But it is not still working... i don't know what id have done wrong now, any django/postgres expert here kindly help me out.

There are several problems with your function:
Statements need to be terminated with a ; - always
Variable assignments are done using := (see: http://www.postgresql.org/docs/current/static/plpgsql-statements.html#PLPGSQL-STATEMENTS-ASSIGNMENT)
You can't return more than one value from a function (unless you create a set returning function, return an object or use out parameters)
Boolean values are true or false. Not 0 or 1 (those are numbers)
The result of an automatically generated ID value is better obtained using lastval() or `` INSERT ... RETURNING expressions INTO ...not through aSET` statement.
Exception handling is done using the exception clause as documented in the manual
So you need something like this:
DECLARE
....
BEGIN
BEGIN
INSERT INTO tbl_messages
(message_subject, message_content, message_type, message_category,
created_on, created_by, updated_on, updated_by)
VALUES
(msg_sub, msg_cont, msg_type,msg_category, LOCALTIMESTAMP,
msg_created_by, LOCALTIMESTAMP, msg_updated_by)
returning message_id
into msg_id;
success := true;
EXCEPTION
WHEN others then
success := false;
msg_id := null;
END;
return msg_id;
END;
But as I said: you can't return more than one value from a function. The only way to do this is to declare OUT parameters, but personally I find them a bit hard to handle in SQL clients.
You have the following options to report an error to the caller:
let the caller handle the exception/error that might arise (which is what I prefer)
define a new user defined data type that contains the message_id and the success flag and return that (but that means you lose the error message!)
return a NULL for the message_id to indicate that something went wrong (but that also means you lose the error information)
Use out parameters to pass both values. An example is available in the manual: http://www.postgresql.org/docs/current/static/xfunc-sql.html#XFUNC-OUTPUT-PARAMETERS

CREATE FUNCTION fn_save_message3(IN msg_sub character varying, IN msg_cont text, IN msg_type character varying, IN msg_category character varying, IN msg_created_by character varying, IN msg_updated_by character varying) RETURNS integer AS
$BODY$ DECLARE msg_id integer := 0;
BEGIN
INSERT INTO tbl_messages
(message_subject, message_content, message_type, message_category,
created_on, created_by, updated_on, updated_by)
VALUES
(msg_sub, msg_cont, msg_type, msg_category, LOCALTIMESTAMP,
msg_created_by, LOCALTIMESTAMP, msg_updated_by);
Select into msg_id currval('tbl_messages_message_id_seq');
return msg_id;
END;$BODY$
LANGUAGE plpgsql VOLATILE NOT LEAKPROOF
COST 100;
ALTER FUNCTION public.fn_save_message(IN character varying, IN text, IN character varying, IN character varying, IN character varying, IN character varying)
OWNER TO gljsxdlvpgfvui;
SELECT fn_save_message3('Test','fjaksdjflksadjflas','email','news','taqi#gmail.com','');

Related

Determine if string is valid XML in Postgres

I have a text field in a table that contains JSON data as well as XML data. As I want to work with XML data only if it's valid XML, I want a way to make sure I can cast the string as XML without producing an error when '{"key":"val"}'::XML is possible.
Basically I want a function select isxml('{"key":"val"}) to return false, and select isxml('<key>1</key>') to be true.
I checked existing Postgres functions such as xml_is_well_formed, but they still return true when checking JSON strings. Maybe I can catch the error and deal with it in exceptions after a bad cast? Is there a good way to do this?
One possibility would be to use the xml_is_well_formed together with a function that checks whether or not the text content is a valid json. I.e.:
create or replace function is_valid_json(content text)
returns boolean
as
$$
begin
return (content::json is not null);
exception
when others then
return false;
end;
And in your query, you do [...] xml_is_well_formed(content) and not is_valid_json(content) [...].
My temporary solution is as follows
CREATE OR REPLACE FUNCTION isjson(p_json text)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS $function$
begin
perform (p_json::json is not null);
return 1;
exception
when others then
return 0;
end;
$function$;
CREATE OR REPLACE FUNCTION isxml(p_xml text)
RETURNS boolean
LANGUAGE plpgsql
IMMUTABLE
AS $function$
BEGIN
PERFORM (p_xml::XML IS NOT NULL);
IF (xml_is_well_formed(p_xml)
AND NOT (CASE WHEN isjson(p_xml) = 1 THEN TRUE ELSE false END)
AND (SELECT p_xml ~ '^<.*>') )THEN -- regex matches <>, this may have uncovered edge cases
RETURN true;
ELSE
RETURN false;
END IF;
EXCEPTION
WHEN OTHERS THEN
RETURN false;
END;
$function$;
Note: my isjson function returns integer due to other legacy compatibility reasons, it would be easier to use boolean for this specific case. This should rule out most problematic cases but have lots of limitations in the regex used, accepting suggestions for improvement.

How to convert text field with formatted currency to numeric field type in Postgres?

I have a table that has a text field which has formatted strings that represent money.
For example, it will have values like this, but also have "bad" invalid data as well
$5.55
$100050.44
over 10,000
$550
my money
570.00
I want to convert this to a numeric field but maintain the actual numbers that can be retained, and for any that can't , convert to null.
I was using this function originally which did convert clean numbers (numbers that didn't have any formatting). The issue was that it would not convert $5.55 as an example and set this to null.
CREATE OR REPLACE FUNCTION public.cast_text_to_numeric(
v_input text)
RETURNS numeric
LANGUAGE 'plpgsql'
COST 100
VOLATILE
AS $BODY$
declare v_output numeric default null;
begin
begin
v_output := v_input::numeric;
exception when others then return null;
end;
return v_output;
end;
$BODY$;
I then created a simple update statement which removes the all non digit characters, but keeps the period.
update public.numbertesting set field_1=regexp_replace(field_1,'[^\w.]','','g')
and if I run this statement, it correctly converts the text data to numeric and maintains the number:
alter table public.numbertesting
alter column field_1 type numeric
using field_1::numeric
But I need to use the function in order to properly discard any bad data and set those values to null.
Even after I run the clean up to set the text value to say 5.55
my "cast_text_to_numeric" function STILL sets this to null ? I don't understand why this sets it to null, but the above statement correctly converts it to a proper number.
How can I fix my cast_text_to_numeric function to properly convert values such as 5.55 , etc?
I'm ok with disgarding (setting to NULL) any values that don't end up with numbers and a period. The regular expression will strip out all other characters... and if there happens to be two numbers in the text field, with the script, they would be combined into one (spaces are removed) and I'm good with that.
In the example of data above, after conversion, the end result in numeric field would be:
5.55
100050.44
null
550
null
570.00
FYI, I am on Postgres 11 right now

How to transfer data with multiple conditions with pgsql?

I would like to create a trigger with I can check the uploaded data and I can insert into another table as well. In the initial table (capture) I have 15 columns, but I would like to transfer only 5 columns (ring_number, code, year, date, species, location) to another table (ring).
The ring table is a background table in which I am collecting the combinations of ring_number and code, more specifically one ring_number could be paired only with one code. There is one exception, when the code include "X", than it can be changed later, and in this case this code can be paired with more ring_number, and if originally belongs to the ring_number a code with "X" it can be changed later.
In the capture table, could be possible to upload the same combination of code and ring_number multiple times with a condition of a third column. But still the ring_number can be paired only with one code, with exceptions of codes included "X". The name of the conditional column is recapture. If recapture (boolean column type) is "true", then you can upload the combination of code and ring_number again. If it is "empty" or "no" you can upload only new combinations of code and ring_number. If somebody uploads old combinations then the following error message has to raise: this combination already exists, please check your data and if it is a recapture, then set the recapture column to yes.
Additionally: ring_number is a not null column, but code can be empty. And different ring_number can be paired with empty code than later can be paired with actual value.
I have several problems with my code:
1: I would like to define the exception to X with regex, and the X can be anywhere in the code. But can not manage the regex in a good way. It does just not work.
2: I write conditional checkpoint with recapture column and if I have an old combination this is work on the right way and say please set the recapture column to yes. But! If I set the recapture column to yes I get the same error message.
Could you help to solve these issues?
Here is my code:
Declare
a integer := 0;
b integer := 0;
c integer := 0;
d integer := 0;
Begin
IF new.code <> '' THEN
--Az 'a' means whether the given ring_number already exist in the database with a code, which is not empty
SELECT INTO a COUNT(*) FROM plover_captures PC WHERE PC.ring_number = new.ring_number AND PC.code <> new.code AND PC.code <> '' AND PC.code ~ '[X]{2}[\.]{1}[X]{2}[|]{1}[X]{2}[\.]{1}[X]{2}';
--Az 'b' means the given code already exist in the database with a ring_number
SELECT INTO b COUNT(*) FROM plover_captures PC WHERE PC.ring_number <> new.ring_number AND PC.code = new.code AND PC.code ~ '[X]{2}[\.]{1}[X]{2}[|]{1}[X]{2}[\.]{1}[X]{2}';
--Az 'c' how much times exist the given ring_number with the given code in the database
SELECT INTO c COUNT(*) FROM plover_captures PC WHERE PC.ring_number = new.ring_number AND PC.code = new.code AND PC.code ~ '[X]{2}[\.]{1}[X]{2}[|]{1}[X]{2}[\.]{1}[X]{2}';
--Az 'd' means the given combination already exist in ring table or not
SELECT INTO d COUNT(*) FROM plover_rings PC WHERE PC.ring_number = new.ring_number AND PC.code = new.code;
IF a > 0 THEN
raise exception 'This ring_number is already paired with another code before. %', new.ring_number;
END IF;
IF b > 0 THEN
raise exception 'This code is already paired with another ring_number before. %', new.code;
END IF;
IF c > 0 AND (new.rettrap IS null OR new.rettrap IS false) THEN
raise exception 'This ring_number and code pair is already in the database. So it is a rettrap but the rettrap attribute set to false or null. %, %, %', new.ring_number, new.code, new.rettrap;
END IF;
IF c = 0 AND new.rettrap IS true THEN
raise exception 'The rettrap attribute set to true but this ring_number and code pair is not in this database yet. %, %, %', new.ring_number, new.code, new.rettrap;
END IF;
IF c = 0 AND d = 0 THEN
Insert into plover_rings values(new.ring_number,new.code,new.species,new.location,new.year, new.date);
END IF;
END IF;
Return new;
End

How to handle redundant types in Crystal?

I'm using the crystal language, and it's been great so far. Unfortunately, I feel like my code is becoming a bit too messy with types everywhere.
For example:
# ---------=====----++---
# Grab characters
# ---------=====----++---
def handle_character_list(msg, client)
result = {} of String => Array(Tuple(Int64, String, String, Int16, Int8)) | Int32 | Int16 | Int64 | String
result["characters"] = db.query_all "select character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc from rpg_characters where user_id = ? ", client.user_id,
as: {Int64, String, String, Int16, Int8}
result["max_char_slots"] = client.user_data["max_char_slots"]
puts result
end
While looking at the db.query_all method, it says:
returns an array where the value of each row is read as the given type
With the aforementioned above, why do I need to explicitly set those types again to my result variable, if they are going to be returned?
I feel like I'm doing something wrong, and any advice/insight is appreciated.
The first thing that jumps out at me is the size of the type of your hash. You seem to be using Hash the same way as in Ruby. Don't.
In Ruby, or other dynamic languages, Hashes or objects are used as generic data containers, almost like unnamed classes. In Crystal, hashes have a single type for the key, and a single type for the value, which makes them unsuited for the task. You want to tell Crystal more about the structure of your data, so you don't have to keep repeating that.
The first thing to do is look at the result object. It can simply be transformed into a record Result:
record Result,
characters: Array({Int64, String, String, Int16, Int8}),
max_char_slots: Int32
the method then becomes
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
characters = db.query_all sql, client.user_id, as: {Int64, String, String, Int16, Int8}
max_char_slots = client.user_data["max_char_slots"]
Result.new(characters, max_char_slots)
end
However, by looking at the method it might be that this Result record is only used in one place - to return data from this method. In that case it's unlikely you want to give it a more formal name. In this case you could use a NamedTuple. They're a bit like an anonymous record.
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
{
characters: db.query_all(sql, client.user_id, as: {Int64, String, String, Int16, Int8}),
max_char_slots: client.user_data["max_char_slots"]
}
end
Going further, we can see that a "Character" is also a type:
class Character
getter id : Int64
getter name : String
getter created : Time
getter level : Int16
getter cc : Int8
def initialize(#id, #name, #created, #level, #cc)
end
end
We can then use the DB.mapping to define how the Character class looks in the database.
class Character
DB.mapping({
id: Int64,
name: String.
created: Time,
level: Int16,
cc: Int8
})
def initialize(#id, #name, #created, #level, #cc)
end
end
This definition is equivalent to the previous one because DB.mapping automatically generates getters for us.
def handle_character_list(msg, client)
sql = <<-SQL
SELECT character_id, character_name, created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
{
characters: db.query_all(sql, client.user_id, as: Character),
max_char_slots: client.user_data["max_char_slots"]
}
end
Going even further, I'd extract that into two methods, each one doing just one thing, and I'd probably make client.user_data more type safe:
def characters_for_user(user_id)
sql = <<-SQL
SELECT character_id, character_name, created, level, cc
FROM rpg_characters
WHERE user_id = ?
SQL
db.query_all(sql, user_id, as: Character)
end
def handle_character_list(msg, client)
{
characters: characters_for_user(client.user_id),
max_character_slots: client.user_data.max_char_slots
}
end
This is just my thought process on how I'd write the code you've shown. I've made a lot of assumptions about your code and database which might be wrong (i.e. "created" is a DATETIME in mysql). I'm attempting to show a thought process, not a finished solution. Hope that helps.

Progress 4GL web service procedure

I'm trying to update our stocks thought the Magento API using Progress 4GL (OpenEdge 10.2B)
So far so good, ie: I can update the stock if the SKU is a match. But if it isn't, it doesn't return an error.
So I looked into how ABL manages SAOP fault errors, and found some examples which I tried to implement. But to no avail.
My new code is as follows:
DEFINE VARIABLE hWebService AS HANDLE NO-UNDO.
DEFINE VARIABLE hMage_Api_Model_Server_V2_HandlerPortType AS HANDLE NO-UNDO.
DEFINE VARIABLE username AS CHARACTER NO-UNDO.
DEFINE VARIABLE apiKey AS CHARACTER NO-UNDO.
DEFINE VARIABLE stock AS CHARACTER NO-UNDO.
DEFINE VARIABLE codigo AS CHARACTER NO-UNDO.
DEFINE VARIABLE loginReturn AS CHARACTER NO-UNDO.
DEFINE VARIABLE product AS CHARACTER NO-UNDO.
DEFINE VARIABLE data AS LONGCHAR NO-UNDO.
DEFINE VARIABLE resultado AS INTEGER NO-UNDO.
DEFINE VARIABLE SOAP-FAULT-CODE AS CHARACTER NO-UNDO.
DEFINE VARIABLE SOAP-FAULT-DETAIL AS CHARACTER NO-UNDO.
DEFINE VARIABLE iError AS INTEGER NO-UNDO.
DEFINE VARIABLE cError AS CHARACTER NO-UNDO.
DO ON ERROR UNDO, THROW:
CREATE SERVER hWebService.
/* TODO: Definir variaveis globais */
username = 'username'.
apiKey = 'password'.
hWebService:CONNECT(" -WSDL 'http://www.medicalemcasa.com/api/v2_soap?wsdl'").
RUN Mage_Api_Model_Server_V2_HandlerPortType SET hMage_Api_Model_Server_V2_HandlerPortType ON hWebService.
RUN login IN hMage_Api_Model_Server_V2_HandlerPortType(INPUT username, INPUT apiKey, OUTPUT loginReturn).
product = "100asda001a".
data = "
<data>
<qty>'250'</qty>
</data>
".
PROCEDURE catalogInventoryStockItemUpdate:
DEFINE INPUT PARAMETER data AS CHARACTER NO-UNDO.
END PROCEDURE.
RUN catalogInventoryStockItemUpdate IN hMage_Api_Model_Server_V2_HandlerPortType (INPUT loginReturn, INPUT product, INPUT data, OUTPUT resultado).
DISPLAY resultado.
CATCH mySoapErrorObject AS Progress.Lang.SoapFaultError:
DO iError = 1 TO mySoapErrorObject:NumMessages:
cError = cError + mySoapErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySoapErrorObject.
END CATCH.
CATCH mySystemErrorObject AS Progress.Lang.SysError:
DO iError = 1 TO mySystemErrorObject:NumMessages:
cError = cError + mySystemErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySystemErrorObject.
END CATCH.
FINALLY:
IF cError <> "" THEN DO:
MESSAGE "Errors occured:" SKIP
cError
VIEW-AS ALERT-BOX ERROR.
END.
END FINALLY.
END.
hWebService:DISCONNECT().
DELETE OBJECT hWebService.
In those API's I've worked with SOAP errors only occur when there's a "bigger" error. For instance if the webservice is down, login criteria isn't met, datatypes are wrong etc. Usually a return value is rather in the response and not in the SOAP-envelope.
Could it be that it's simply OK to set a non existent product to inventory 0? What happens if you try to set it to 1? Perhaps you should double check from PHP (or whatever language you usually work with) that the web service actually provides the code you expect in this case?
Otherwise you should look at the wsdl-documentation created - are you 100% sure that the result-parameter (resultado in you code) is an INTEGER and not any form of more complicated xml-document (an object basically)? If it's really a HANDLE it might be that there's no run time error but no value is inserted into the INTEGER.
Also you should remove all your current error handling and replace it with a more general way to handle errors (and make that code more specific if needed rather than working from an example out of the documentation):
DEFINE VARIABLE iError AS INTEGER NO-UNDO.
DEFINE VARIABLE cError AS CHARACTER NO-UNDO.
CATCH mySoapErrorObject AS Progress.Lang.SoapFaultError:
DO iError = 1 TO mySoapErrorObject:NumMessages:
cError = cError + mySoapErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySoapErrorObject.
END CATCH.
CATCH mySystemErrorObject AS Progress.Lang.SysError:
DO iError = 1 TO mySystemErrorObject:NumMessages:
cError = cError + mySystemErrorObject:getMessage(iError) + "~n".
END.
DELETE OBJECT mySystemErrorObject.
END CATCH.
And insert in the FINALLY-block:
IF cError <> "" THEN DO:
MESSAGE "Errors occured:" SKIP
cError
VIEW-AS ALERT-BOX ERROR.
END.