How to check updating of column value in oracle trigger - sql

I'm using UPDATING(col_name) to check if column's value is updated or not inside the trigger. But the big problem is this command won't check value of :old and :new objects. UPDATING(col_name) is true if col_name existed in set part of query even with old value.
I don't want to check :old.col1<>:new.col1 for each column separately.
How can I check changing column value correctly?
I want to do this in a generic way. like :
SELECT col_name bulk collect INTO included_columns FROM trigger_columns where tbl_name ='name';
l_idx := included_columns.first;
while (l_idx is not null)
loop
IF UPDATING(included_columns(l_idx)) THEN
//DO STH
return;
END IF;
l_idx := included_columns.next(l_idx);
end loop;
Thanks

IN a comment you said:
"I want to do this in a generic way and manage it safer. put columns which are important to trigger in a table and don't put many IF in my trigger. "
I suspected that was what you wanted. The only way you can make that work is to use dynamic SQL to assemble and execute a PL/SQL block. That is a complicated solution, for no material benefit.
I'm afraid I laughed at your use of "safer" there. Triggers are already horrible: they make it harder to reason about what is happening in the database and can lead to unforeseen scalability issues. Don't make them worse by injecting dynamic SQL into the mix. Dynamic SQL is difficult because it turns compilation errors into runtime errors.
What is your objection to hardcoding column names and IF statements in a trigger? It's safer because the trigger is compiled. It's easier to verify the trigger logic because the code is right there.
If this is just about not wanting to type, then you can generate the trigger source from the data dictionary views (such as all_tab_cols) or even your own metadata tables if you must (i.e. trigger_columns).

You can define a global function similar to the following:
CREATE OR REPLACE FUNCTION NUMBER_HAS_CHANGED(pinVal_1 IN NUMBER,
pinVal_2 IN NUMBER)
RETURN CHAR
IS
BEGIN
IF (pinVal_1 IS NULL AND pinVal_2 IS NOT NULL) OR
(pinVal_1 IS NOT NULL AND pinVal_2 IS NULL) OR
pinVal_1 <> pinVal_2
THEN
RETURN 'Y';
ELSE
RETURN 'N';
END IF;
END NUMBER_HAS_CHANGED;
Now in your trigger you just write
IF NUMBER_HAS_CHANGED(:OLD.COL1, :NEW.COL1) = 'Y' THEN
-- whatever
END IF;
Note that this function is defined to return CHAR so it can also be called from SQL statements, if needed - for example, in a CASE expression. Remember that in Oracle, there is no BOOLEAN type in the database - only in PL/SQL.
You'll probably want to create additional versions of this function to handle VARCHAR2 and DATE values, for a start, but since it's a matter of replacing the data types and changing the name of the function I'll let you have the fun of writing them. :-)
Best of luck.

Related

Struggling to create a "stored procedure" beyond INSERT

Whenever I try to call a stored procedure in PostgreSQL that goes beyond inserting data, it takes forever to run, and it isn't that the query is complicated or the dataset is huge. The dataset is small. I cannot return a table from a stored procedure and I cannot even return 1 row or 1 data point from a stored procedure. It says it is executing the query for a very long time until I finally stop the query from running. It does not give me a reason. I can't let it run for hours. Any ideas on what might be happening? I have included stored procedures that I have tried to call.
Non-working example #1:
CREATE PROCEDURE max_duration(OUT maxD INTERVAL)
LANGUAGE plpgsql AS $$
DECLARE maxD INTERVAL;
BEGIN
SELECT max(public.bikeshare3.duration)
INTO maxD
FROM public.bikeshare3;
END;
$$ ;
CALL max_duration(NULL);
Non-working example #2:
CREATE PROCEDURE getDataByRideId2(rideId varchar(16))
LANGUAGE SQL
AS $$
SELECT rideable_type FROM bikeshare3
WHERE ride_id = rideId
$$;
CALL getDataByRideId2('x78900');
Working example
The only one that worked when called is an insert procedure:
CREATE OR REPLACE PROCEDURE genre_insert_data(GenreId integer, Name_b character varying)
LANGUAGE SQL
AS $$
INSERT INTO public.bikeshare3 VALUES (GenreId, Name_b)
$$;
CALL genre_insert_data(1, 'testName');
FUNCTION or PROCEDURE?
The term "stored procedure" has been a widespread misnomer for the longest time. That got more confusing since Postgres 11 added CREATE PROCEDURE.
You can create a FUNCTION or a PROCEDURE in Postgres. Typically, you want a FUNCTION. A PROCEDURE mostly only makes sense when you need to COMMIT in the body. See:
How to return a value from a stored procedure (not function)?
Nothing in your question indicates the need for a PROCEDURE. You probably want a FUNCTION.
Question asked
Adrian already pointed out most of what's wrong in his comment.
Your first example could work like this:
CREATE OR REPLACE PROCEDURE max_duration(INOUT _max_d interval = NULL)
LANGUAGE plpgsql AS
$proc$
BEGIN
SELECT max(b.duration) INTO _max_d
FROM public.bikeshare3 b;
END
$proc$;
CALL max_duration();
Most importantly, your OUT parameter is visible inside the procedure body. Declaring another instance as variable hides the parameter. You could access the parameter by qualifying with the function name, max_duration.maxD in your original. But that's a measure of last resort. Rather don't introduce duplicate variable names to begin with.
I also did away with error-prone mixed-case identifiers in my answer. See:
Are PostgreSQL column names case-sensitive?
I made the parameter INOUT max_d interval = NULL. By adding a default value, we don't have to pass a value in the call (that's not used anyway). But it must be INOUT instead of OUT for this.
Also, OUT parameters only work for a PROCEDURE since Postgres 14. The release notes:
Stored procedures can now return data via OUT parameters.
While using an OUT parameter, this advise from the manual applies:
Arguments must be supplied for all procedure parameters that lack
defaults, including OUT parameters. However, arguments matching OUT
parameters are not evaluated, so it's customary to just write NULL
for them. (Writing something else for an OUT parameter might cause
compatibility problems with future PostgreSQL versions.)
Your second example could work like this:
CREATE OR REPLACE PROCEDURE get_data_by_ride_id2(IN _ride_id text
, INOUT _rideable_type text = NULL) -- return type?
LANGUAGE sql AS
$proc$
SELECT b.rideable_type
FROM public.bikeshare3 b
WHERE b.ride_id = _ride_id;
$proc$;
CALL get_data_by_ride_id2('x78900');
If the query returns multiple rows, only the first one is returned and the rest is discarded. Don't go there. This only makes sense while ride_id is UNIQUE. Even then, a FUNCTION still seems more suitable ...

pl/sql procedure with variable numbers of parameters

I want to know if I can create a PL/SQL procedure that the number of parameters and their types changes.
For example procedure p1.
I can use it like this
p1 (param1, param2,......., param n);
i want to pass table name and data in procedure, but the attributes change for every table,
create or replace PROCEDURE INSERTDATA(NOMT in varchar2) is
num int;
BEGIN
EXECUTE IMMEDIATE 'SELECT count(*) FROM user_tables WHERE table_name = :1'
into num using NOMT ;
IF( num < 1 )
THEN
dbms_output.put_line('table not exist !!! ');
ELSE
dbms_output.put_line('');
-- here i want to insert parameters in the table,
-- but the table attributes are not the same !!
END IF;
NULL;
END INSERTDATA;
As far as I can tell, no, you can not. Number and datatypes of all parameters must be fixed.
You could pass a collection as a parameter (and have different number of values within it), but - that's still a single parameter.
Where would you want to use such a procedure?
If you need to store, update and query a variable amount of information, might I recommend switching to JSON queries and objects in Oracle. Oracle has deep support for both fixed and dynamic querying of json data, both in SQL and PLSQL.
i want to pass table name and data in procedure, but the attributes change for every table,
The problem with such a universal procedure is that something needs to know the structure of the target table. Your approach demands that the caller has to discover the projection of the table and arrange the parameters in a correct fashion.
In no particular order:
This is bad practice because it requires the calling program to do the hard work regarding the data dictionary.
Furthermore it breaks the Law Of Demeter because the calling program needs to understand things like primary keys (sequences, identity columns, etc), foreign key lookups, etc
This approach mandates that all columns must be populated; it makes no allowance for virtual columns, optional columns, etc
To work the procedure would have to use dynamic SQL, which is always hard work because it turns compilation errors into runtime errors, and should be avoided if at all possible.
It is trivially simple to generate a dedicated insert procedure for each table in a schema, using dynamic SQL against the data dictionary. This is the concept of the Table API. It's not without its own issues but it is much safer than what your question proposes.

ORA 00920 In CASE with "OR" inside Trigger Oracle

The syntax error seems to be in the WHEN INSERTING OR UPDATING (more specifically it underlines the OR), I don't understand why it doesn't work in a CASE condition but it works in a IF condition.
Its a BEFORE DELETE OR INSERT OR UPDATE Trigger
Code portion of the issue:
SELECT
field
INTO
my_var
FROM
my_table
WHERE
column1 = (CASE
WHEN INSERTING OR UPDATING THEN
:new.column2
ELSE --deleting
:old.column2
END);
What would be the solution?
Here is the rest of the trigger if anyone wants to test : https://pastebin.com/AJqGQyG8
Edit :
The issue seems to be that the WHEN condition needs an operator, so I tried using :
WHEN INSERTING = TRUE
But that just resulted in another error :
ORA-00904: "TRUE": Invalid identified
As Thorsten explained already, inserting and updating and deleting are predicates (expressions of BOOLEAN data type) which exist only in the PL/SQL code part of the trigger. They can't exist (or be used in any way) in a SQL statement, since Oracle SQL does not recognize/implement the BOOLEAN data type.
Moreover, :new.<whatever> and :old.whatever are also available only in the PL/SQL portion of your trigger; if you have embedded SQL, anything like :new.<whatever> is interpreted as a bind variable, not special in any way, and you will be prompted for a value (if you use an "advanced" front-end program) or you will simply get an error telling you that not all variables are bound.
Thorsten has already shown one way to do what you want to do. If you really need (want?) to use a case expression, you can do something like this:
Declare a variable (like v_value in Thorsten's answer) in the DECLARE section of the trigger. Assign the proper data type to it.
Then in the execution block write something like this, BEFORE the SQL statement:
v_value := case when inserting or updating then :new.column2 else :old.column2 end;
and then in the SQL statement compare to v_value, also as in Thorsten's answer.
Alternatively, you can assign the value to v_value in the DECLARE block, right as you declare it, in the usual (PL/SQL) way. In any case, the value must be assigned outside the SQL statement, not in it.
I think the problem is that PL/SQL knows these boolean variables inside a trigger, but Oracle's SQL doesn't know BOOLEAN unfortunately.
One solution may be:
IF INSERTING OR UPDATING THEN
v_value := :new.column2;
ELSE
v_value := :old.column2;
END IF;
...
SELECT field INTO my_var FROM my_table WHERE column1 = v_value;

Inserting a record in sybase db table using stored procedure - delphi programming

I am new at programming with delphi. I am currently creating a simple notebook program and i need some help. I have a form called contacts with 5 tEdit fields. I am thinking i could create a stored procedure in my sybase database to insert record into Contacts table, so I can call it with my delphi programm. How do I call this procedure in delphi? the values that will be inserted should be taken from users input into these tEdit fields. Anyone has any suggestions? Or am I thinking the wrong way? thanks in advance
You have several options here, and it will depend on what VCL controls you are using.
(1). You can insert via a tTable component. This let's you have a quick, easy, low level control. You drop the component on the form, set the component properties (tablename, etc), then something like
MyTable.Open;
MyTable.Insert; (or maybe append)
MyTable.FieldByName('MY_FIELD').AsString := 'Bob'; // set the field values
MyTable.post;
(2). Use SQL. Drop a SQL component on the form. Set the SQLText property, using parameters;
for example : "Insert into table (MyField) values :X". My opinion is that this is easier to do in complex situations, correlated subselects, etc.
MySQL.Close;
MySQL.ParamByName('X').AsString := 'BOB';
ExecSQL;
(3). Use stored procedures. - The advantage to this is that they are useable by multiple applications, and can be changed easily. If you want to update the SQL code, you update it once (in the database), versus having to change it in an app, and then distribute the app to multiple users.
The code for this will be nearly identify to (2), although I don't know the specifics of your VCL library. In effect though, you will specify the routine to run, specify the parameter values, and then execute the stored procedure.
Note that all these routines will return an error code or exception code. It is best practice to always check for that...
Here is a little more complex example, using a SQL statement called qLoader. qLoader exists on a datamodule. I am passing a parameter, executing the SQL statement, then iterating through all the results.
try
with dmXLate.qLoader do
begin
Close;
ParamByName('DBTYPE').AsString := DBType;
Open;
while not dmXLate.qLoader.Eof do
begin
// Here is where we process each result
UserName:= dmXLate.qLoader.FieldByName('USERNAME').AsString;
dmXLate.qLoader.Next;
end;
end;
except
on E: Exception do
begin
ShowMEssage(E.Message);
exit;
end;
end;

Call Function within User-Defined Function in SQL

I want to include the following function inside of another user-defined function in Oracle.
DBMS_STATS.GATHER_TABLE_STATS(SCHEMA_IN,TABLE_IN)
SCHEMA_IN and TABLE_IN are arguments to the user-defined function. However, I get the following error.
ORA-14552: cannot perform a DDL, commit or rollback inside a query or DML
How can I resolve this? Below is my SQL script.
CREATE OR REPLACE Function GET_COLUMNS (SCHEMA_IN IN VARCHAR2, NAME_IN IN VARCHAR2)
RETURN VARCHAR2
is
L_TEXT VARCHAR2(32767) := NULL;
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(SCHEMA_IN,NAME_IN);
FOR CUR_REC IN (SELECT COLUMN_NAME FROM USER_TAB_COLUMNS WHERE TABLE_NAME = name_in AND NUM_NULLS = 0) LOOP
L_TEXT := L_TEXT || ',' || CUR_REC.COLUMN_NAME;
END LOOP;
return(ltrim(l_text,','));
END;
gather_table_stats is a procedure, not a function. And it a procedure that includes transaction control logic (presumably, a commit at least). You cannot, therefore, call it in a function that is called from SQL. You could call your function from PL/SQL rather than SQL,
DECLARE
l_text varchar2(4000);
BEGIN
l_text := get_columns( <<schema>>, <<table>> );
END;
I would, however, be very, very dubious about the approach you're taking.
First, dbms_stats gathers statistics that are used by the optimizer. Using those statistics in other contexts is generally dangerous. Most dbms_stats calls involve some level of indeterminism-- you're generally gathering data from a sample of rows and extrapolating. That is perfectly appropriate for giving the optimizer information so that it can judge things like roughly how many rows a table scan will return. It may not be appropriate if you're trying to differentiate between a column that is never NULL and one that is very rarely NULL. Some samples may catch a NULL value, others may not. It may seem to work correctly for months or years and then start to fail either consistently or intermittantly.
Second, when you gather fresh statistics, you're potentially forcing Oracle to do hard parses on all the existing SQL statements that reference the table. That can be a major performance hit if you do this in the middle of the day. If you happen to force a query plan to change in a bad way, you'll likely cause the DBA a great deal of grief. If the DBA is gathering statistics in a particular way (locking statistics on some tables, forcing histograms on others, forcing a lack of histograms on others, etc.) to deal with performance issues, it's highly likely that you'll be either working at cross purposes or actively breaking the other.
Third, if a column never has NULL values, it really ought to be marked as NOT NULL. Then you can simply look at the data dictionary to see which columns are nullable and which are not without bothering to gather statistics.
You need to set your function to be an autonomous transaction to execute gather table stats:
CREATE OR REPLACE Function GET_COLUMNS (SCHEMA_IN IN VARCHAR2, NAME_IN IN VARCHAR2)
RETURN VARCHAR2
as
pragma autonomous_transaction;
L_TEXT VARCHAR2(32767) := NULL;
BEGIN