How can I make a unique constraint order independently on two columns - sql

I'm working with an Oracle Database and I need to create a table like below.
MAP(Point_One, Poin_Two, Connection_weight).
The table represents data about a graph. I would like to create a table with a constraint that prevents the insertion of an already existing connection.
For example, the table already contains this connection:
Point_One | Point_Two | Connection_weight
-----------------------------------------
p_no1 | p_no2 | 10
And the constraint would prevent the repeated insertion of this connection, even if I try to add the points in different order. (For example: (p_no2, p_no1, 10) )
A simple UNIQUE (Point_One, Point_Two) constraint is unfortunatelly not enough. Do you have any advice?

You can create a function-based index
CREATE UNIQUE INDEX idx_unique_edge
ON map( greatest( point_one, point_two ),
least( point_one, point_two ) );
I'm assuming that the data type of point_one and point_two is compatible with the Oracle greatest and least functions. If not, you'd need a function of your own that picks the "greatest" and "least" point for your complex data type.

You can achieve the desired result easily by using trigger.
create table map (point_one number, point_two number, connection_weight number)
/
create or replace trigger tr_map before insert on map
for each row
declare
c number;
begin
select count(1) into c from map where (point_one=:new.point_one and point_two=:new.point_two)
or
(point_one=:new.point_two and point_two=:new.point_one);
if c>0 then
raise_application_error(-20000,'Connection line already exists');
end if;
end;
/
SQL> insert into map values (1,2,10);
1 row created.
SQL> insert into map values (2,1,10);
insert into map values (2,1,10)
*
ERROR at line 1:
ORA-21000: error number argument to raise_application_error of -100 is out of
range
ORA-06512: at "C##MINA.TR_MAP", line 10
ORA-04088: error during execution of trigger 'C##MINA.TR_MAP'
I'm still thinking about the CHECK constraint, but yet I didn't come with decission whether it is possible or not.

Related

oracle constraints datatype

I have created a table with datatype smallint.
create table test(
A smallint,
constraints ACHECK check(A between 1 and 5));
I want to add constraint that only allow users to add integer value range between 1~5.
But, even with the constraints, I am still able to insert floating point value which gets round up automatically.
insert into test values(3.2);
How do I make this code to show an error?
I am not allow to change datatype.
You cannot do what you want easily. Oracle is converting the input value 3.2 to an integer. The integer meets the constraint. The value 3 is what gets inserted. The conversion happens behind the scenes. The developers of Oracle figured this conversion is a "good thing".
You could get around this by declaring the column as a number and then checking that it is an integer:
create table test (
A number,
constraints ACHECK check(A between 1 and 5 and mod(A, 1) = 0)
);
As far as the requirement is NOT TO CHANGE THE DATA TYPE, but it doesn't say anything regarding creating new objects, I came up with a very complicated solution which does the trick but I would have preferred by far to change the data type to number and use a normal constraint.
The main problem here is that the round up of the value is done after parsing the statement, but before executing. As is an internal mechanism , you can't do anything about it. You can easily see that happening if you use a trigger and display the value of :NEW before inserting or updating the column.
However, there is a trick. F.G.A. got the original value passed to the statement before parsing. So, using a policy with a handler and two triggers make the trick.
Let me go into details
SQL> create table testx ( xsmall smallint );
Table created.
SQL> create table tracex ( id_timestamp timestamp , who_was varchar2(50) , sqltext varchar2(4000) );
Table created.
SQL> create or replace procedure pr_handle_it (object_schema VARCHAR2, object_name VARCHAR2, policy_name VARCHAR2)
is
begin
-- dbms_output.put_line('SQL was: ' || SYS_CONTEXT('userenv','CURRENT_SQL'));
insert into tracex values ( systimestamp , sys_context('userenv','session_user') , sys_context('userenv','current_sql') );
commit;
end;
/
Procedure created.
SQL> BEGIN
DBMS_FGA.ADD_POLICY(
object_schema => 'MYSCHEMA',
object_name => 'TESTX',
policy_name => 'MY_NEW_POLICY',
audit_condition => null,
audit_column => 'XSMALL',
handler_schema => 'MYSCHEMA',
handler_module => 'PR_HANDLE_IT',
enable => true,
statement_types => 'INSERT, UPDATE, DELETE'
);
END;
/
PL/SQL procedure successfully completed.
SQL> create or replace trigger trg_testx before insert or update on testx
referencing new as new old as old
for each row
begin
if inserting or updating
then
dbms_output.put(' New value is: ' || :new.xsmall);
dbms_output.put_line('TRIGGER : The value for CURRENT_SQL is '||sys_context('userenv','current_sql'));
insert into tracex values ( systimestamp , sys_context('userenv','session_user') , sys_context('userenv','current_sql') );
end if;
end;
/
Trigger created.
SQL> create or replace trigger trg_testx2 after insert or update on cpl_rep.testx
referencing new as new old as old
for each row
declare
v_val pls_integer;
begin
if inserting or updating
then
select regexp_replace(sqltext,'[^0-9]+','') into v_val
from ( select upper(sqltext) as sqltext from tracex order by id_timestamp desc ) where rownum = 1 ;
if v_val > 5
then
raise_application_error(-20001,'Number greater than 5 or contains decimals');
end if;
end if;
end ;
/
Trigger created.
These are the elements:
-One trace table to get the query before parsing
-One FGA policy over update and insert
-One procedure for the handler
-Two triggers one before ( got the query and the original value ) and one after to evaluate the value from the statement not the one rounded up.
Due to the fact that the triggers are evaluating in order, the before one inserts the original value with the decimal and it does it before the round up, the after analyses the value stored in the trace table to raise the exception.
SQL> insert into testx values ( 1 ) ;
1 row created.
SQL> insert into testx values ( 5 ) ;
1 row created.
SQL> insert into testx values ( 2.1 ) ;
insert into testx values ( 2.1 )
*
ERROR at line 1:
ORA-20001: Number greater than 5 or it contains decimals
ORA-06512: at "CPL_REP.TRG_TESTX2", line 10
ORA-04088: error during execution of trigger 'CPL_REP.TRG_TESTX2'
SQL> insert into testx values ( 6 ) ;
insert into testx values ( 6 )
*
ERROR at line 1:
ORA-20001: Number greater than 5 or it contains decimals
ORA-06512: at "CPL_REP.TRG_TESTX2", line 10
ORA-04088: error during execution of trigger 'CPL_REP.TRG_TESTX2'
Summary: As #Gordon Linoff said there was no easy way to achieve what was asked. I believe the method is very complicated for the requirement. I just came up with it for the purpose to show that it was possible after all.

Oracle Making local tables using dynamic sql?

How to use local temporary table or alternative using dynamic SQL in Oracle?
In SQL Server, to select all the columns from a table called dbo.2019 into a local temporary table called x:
CREATE TABLE #x (a int)
DECLARE #FY varchar(4) = Year(date())
EXEC ('SELECT * into #x FROM dbo.'+#FY)
The reason why I want to do this is because I want to use all kinds of information (besides year, like values from other tables or whatever) to build very complicated queries in a manner that does not require lots of bizarre subqueries.
The limitation of SQL Server is that you must make your temporary tables first, or you have to use global ones, that people could write over.
As you know, Oracle <> MS SQL Server. The latter uses temporary tables a lot. Oracle - usually/mostly - doesn't need them.
But, for your specific problem ("very complicated queries" and stuff), that maybe isn't a bad idea. Depending on your Oracle database version, there are global temporary tables (and - in most recent versions, private ones). How do they work? You create them once and use many times, which means that you should NOT create them dynamically. Data stored within is visible to you only, although many users can use it simultaneously. Furthermore, data is kept during the whole session (if you opt to create them with the on commit preserve rows option) or during transaction (on commit delete rows).
For example:
SQL> create global temporary table gtt_test
2 (id number,
3 name varchar2(20))
4 on commit delete rows;
Table created.
SQL>
You can index them:
SQL> create index i1_gtt on gtt_test (id);
Index created.
SQL>
and do anything you want. Once your session (or transaction) is over, puff! they are empty, not data is permanently stored within. When you have to do the same job tomorrow, the table is still here, waiting for you. No dynamic create/drop/create/drop (or whatever you thought you should do).
So: create it once, use it any time you need.
If it must be dynamically, beware of this:
SQL> declare
2 l_cnt number;
3 begin
4 -- check whether table already exists
5 select count(*)
6 into l_Cnt
7 from user_tables
8 where table_name = 'TEST';
9
10 -- yes, it exists - drop it first
11 if l_cnt = 1 then
12 execute immediate 'drop table test';
13 end if;
14 -- now create it
15 execute immediate 'create table test (id number, name varchar2(20))';
16
17 -- insert some rows
18 insert into test (id, name) values (1, 'Littlefoot');
19 end;
20 /
insert into test (id, name) values (1, 'Littlefoot');
*
ERROR at line 18:
ORA-06550: line 18, column 15:
PL/SQL: ORA-00942: table or view does not exist
ORA-06550: line 18, column 3:
PL/SQL: SQL Statement ignored
SQL>
Although I'm first checking whether the table exists (so that I'd drop it first and then recreate it), the whole PL/SQL block failed because I'm trying to use a table that doesn't exist yet (at compile time).
So, if I'd want to use that test table, any operation - within the same PL/SQL block - should also become dynamic, and that's horrible: ugly to write, difficult to debug, beware of single quote problems ... you'd really want to avoid that.
Once again: I'd suggest you not to do that dynamically.

Trigger Function → Selecting a unique row using a values in a none unique column

I got a sticky situation here whereby I setup a trigger to update a table (Self-updating function). What I got here is that the function is able to identify that there is an update operation however, it cannot located the row to update as there are no unique value in the column.
TRIG_NS_ABS4_To_Area_func (Trigger Func):
BEGIN
IF (TG_OP = 'UPDATE') AND (OLD."ABS4" <> NEW."ABS4")
THEN UPDATE systems."NS_HandoverReportInput_tbl" SET ("Area") = ((SELECT "NS_AREA" FROM systems."NS_ABS4Area Match_tbl" WHERE "NS_ABS4" = NEW."ABS4"))
WHERE "NSItemNumber" = NEW."NSItemNumber";
END IF;
RETURN NEW;
END;
I was wondering whether does anyone have any idea to locate the row to update.
Please bear in mind that ONLY the "NSItemNumber" field is unique else the rest of the fields may have repeating values.
Script:
CREATE TABLE systems."NS_HandoverReportInput_tbl" (
"NSItemNumber" SERIAL,
"ABS4" TEXT,
"Area" TEXT,
CONSTRAINT "PK_NS_HandoverReportInput_tbl" PRIMARY KEY("NSItemNumber"),
)
WITH (oids = false);
CREATE TRIGGER "NS_ABS4Area Match_tbl"
AFTER INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
NS_ABS4Area Match_tbl display info as listed:
NSItemNumber | ABS4 | Area
1001 | AAAA |Toilet
1002 | AABB |Central Area
1003 | AACC |Carpark
1004 | AAAA |Toilet
1005 | AABB |Central Area
I'll give you two solutions, a working one and a good one.
The working solution
Use a BEFORE INSERT OR UPDATE trigger, then you don't have to locate the row to update, because instead of changing the table you change the values before they are written to the table.
You could define your trigger like this:
CREATE OR REPLACE FUNCTION "TRIG_NS_ABS4_To_Area_func"() RETURNS trigger LANGUAGE plpgsql AS
$$BEGIN
-- this will fail if there is more than one "NS_AREA" per "NS_ABS4"
SELECT DISTINCT "NS_AREA" INTO STRICT NEW."NS_AREA"
FROM "NS_ABS4Area Match_tbl"
WHERE "NS_ABS4" = NEW."ABS4";
RETURN NEW;
END;$$;
CREATE TRIGGER "NS_ABS4Area Match_tbl"
BEFORE INSERT OR UPDATE
ON systems."NS_HandoverReportInput_tbl" FOR EACH ROW
EXECUTE PROCEDURE systems."TRIG_NS_ABS4_To_Area_func"();
The good solution
You avoid the whole mess by normalizing your database design.
That way no inconsistencies can ever happen, and you don't need a trigger.
CREATE TABLE area_description (
abs4 text PRIMARY KEY,
area text NOT NULL
);
COPY area_description FROM STDIN (FORMAT 'csv');
'AAAA', 'Toilet'
'AABB', 'Central Area'
'AACC', 'Carpark'
\.
CREATE TABLE ns_report_input (
ns_item_number serial PRIMARY KEY,
abs4 text REFERENCES area_description(abs4)
);
CREATE INDEX ns_report_input_fkex_ind ON ns_report_input(abs4);
You can define a view if you want something that looks like your original table.

Oracle - Insert New Row with Auto Incremental ID

I have a workqueue table that has a workid column. The workID column has values that increment automatically. Is there a way I can run a query in the backend to insert a new row and have the workID column increment automatically?
When I try to insert a null, it throws error ORA01400 - Cannot insert null into workid.
insert into WORKQUEUE (facilitycode,workaction,description) values ('J', 'II', 'TESTVALUES')
What I have tried so far - I tried to look at the table details and didn't see any auto-increment. The table script is as follow
"WORKID" NUMBER NOT NULL ENABLE,
Database: Oracle 10g
Screenshot of some existing data.
ANSWER:
I have to thank each and everyone for the help. Today was a great learning experience and without your support, I couldn't have done. Bottom line is, I was trying to insert a row into a table that already has sequences and triggers. All I had to do was find the right sequence, for my question, and call that sequence into my query.
The links you all provided me helped me look these sequences up and find the one that is for this workid column. Thanks to you all, I gave everyone a thumbs up, I am able to tackle another dragon today and help patient care take a step forward!"
This is a simple way to do it without any triggers or sequences:
insert into WORKQUEUE (ID, facilitycode, workaction, description)
values ((select max(ID)+1 from WORKQUEUE), 'J', 'II', 'TESTVALUES')
It worked for me but would not work with an empty table, I guess.
To get an auto increment number you need to use a sequence in Oracle.
(See here and here).
CREATE SEQUENCE my_seq;
SELECT my_seq.NEXTVAL FROM DUAL; -- to get the next value
-- use in a trigger for your table demo
CREATE OR REPLACE TRIGGER demo_increment
BEFORE INSERT ON demo
FOR EACH ROW
BEGIN
SELECT my_seq.NEXTVAL
INTO :new.id
FROM dual;
END;
/
There is no built-in auto_increment in Oracle.
You need to use sequences and triggers.
Read here how to do it right. (Step-by-step how-to for "Creating auto-increment columns in Oracle")
ELXAN#DB1> create table cedvel(id integer,ad varchar2(15));
Table created.
ELXAN#DB1> alter table cedvel add constraint pk_ad primary key(id);
Table altered.
ELXAN#DB1> create sequence test_seq start with 1 increment by 1;
Sequence created.
ELXAN#DB1> create or replace trigger ad_insert
before insert on cedvel
REFERENCING NEW AS NEW OLD AS OLD
for each row
begin
select test_seq.nextval into :new.id from dual;
end;
/ 2 3 4 5 6 7 8
Trigger created.
ELXAN#DB1> insert into cedvel (ad) values ('nese');
1 row created.
You can use either SEQUENCE or TRIGGER to increment automatically the value of a given column in your database table however the use of TRIGGERS would be more appropriate. See the following documentation of Oracle that contains major clauses used with triggers with suitable examples.
Use the CREATE TRIGGER statement to create and enable a database trigger, which is:
A stored PL/SQL block associated with a table, a schema, or the
database or
An anonymous PL/SQL block or a call to a procedure implemented in
PL/SQL or Java
Oracle Database automatically executes a trigger when specified conditions occur. See.
Following is a simple TRIGGER just as an example for you that inserts the primary key value in a specified table based on the maximum value of that column. You can modify the schema name, table name etc and use it. Just give it a try.
/*Create a database trigger that generates automatically primary key values on the CITY table using the max function.*/
CREATE OR REPLACE TRIGGER PROJECT.PK_MAX_TRIGGER_CITY
BEFORE INSERT ON PROJECT.CITY
FOR EACH ROW
DECLARE
CNT NUMBER;
PKV CITY.CITY_ID%TYPE;
NO NUMBER;
BEGIN
SELECT COUNT(*)INTO CNT FROM CITY;
IF CNT=0 THEN
PKV:='CT0001';
ELSE
SELECT 'CT'||LPAD(MAX(TO_NUMBER(SUBSTR(CITY_ID,3,LENGTH(CITY_ID)))+1),4,'0') INTO PKV
FROM CITY;
END IF;
:NEW.CITY_ID:=PKV;
END;
Would automatically generates values such as CT0001, CT0002, CT0002 and so on and inserts into the given column of the specified table.
SQL trigger for automatic date generation in oracle table:
CREATE OR REPLACE TRIGGER name_of_trigger
BEFORE INSERT
ON table_name
REFERENCING NEW AS NEW
FOR EACH ROW
BEGIN
SELECT sysdate INTO :NEW.column_name FROM dual;
END;
/
the complete know how, i have included a example of the triggers and sequence
create table temasforo(
idtemasforo NUMBER(5) PRIMARY KEY,
autor VARCHAR2(50) NOT NULL,
fecha DATE DEFAULT (sysdate),
asunto LONG );
create sequence temasforo_seq
start with 1
increment by 1
nomaxvalue;
create or replace
trigger temasforo_trigger
before insert on temasforo
referencing OLD as old NEW as new
for each row
begin
:new.idtemasforo:=temasforo_seq.nextval;
end;
reference:
http://thenullpointerexceptionx.blogspot.mx/2013/06/llaves-primarias-auto-incrementales-en.html
For completeness, I'll mention that Oracle 12c does support this feature. Also it's supposedly faster than the triggers approach. For example:
CREATE TABLE foo
(
id NUMBER GENERATED BY DEFAULT AS IDENTITY (
START WITH 1 NOCACHE ORDER ) NOT NULL ,
name VARCHAR2 (50)
)
LOGGING ;
ALTER TABLE foo ADD CONSTRAINT foo_PK PRIMARY KEY ( id ) ;
Best approach: Get the next value from sequence
The nicest approach is getting the NEXTVAL from the SEQUENCE "associated" with the table. Since the sequence is not directly associated to any specific table,
we will need to manually refer the corresponding table from the sequence name convention.
The sequence name used on a table, if follow the sequence naming convention, will mention the table name inside its name. Something likes <table_name>_SEQ. You will immediately recognize it the moment you see it.
First, check within Oracle system if there is any sequence "associated" to the table
SELECT * FROM all_sequences
WHERE SEQUENCE_OWNER = '<schema_name>';
will present something like this
Grab that SEQUENCE_NAME and evaluate the NEXTVAL of it in your INSERT query
INSERT INTO workqueue(id, value) VALUES (workqueue_seq.NEXTVAL, 'A new value...')
Additional tip
In case you're unsure if this sequence is actually associated with the table, just quickly compare the LAST_NUMBER of the sequence (meaning the current value) with the maximum id of
that table. It's expected that the LAST_NUMBER is greater than or equals to the current maximum id value in the table, as long as the gap is not too suspiciously large.
SELECT LAST_NUMBER
FROM all_sequences
WHERE SEQUENCE_OWNER = '<schema_name>' AND SEQUENCE_NAME = 'workqueue_seq';
SELECT MAX(ID)
FROM workqueue;
Reference: Oracle CURRVAL and NEXTVAL
Alternative approach: Get the current max id from the table
The alternative approach is getting the max value from the table, please refer to Zsolt Sky answer in this same question
This is a simple way to do it without any triggers or sequences:
insert into WORKQUEUE (ID, facilitycode, workaction, description)
values ((select count(1)+1 from WORKQUEUE), 'J', 'II', 'TESTVALUES');
Note : here need to use count(1) in place of max(id) column
It perfectly works for an empty table also.

Oracle - Modify an existing table to auto-increment a column

I have a table with the following column:
NOTEID NUMBER NOT NULL,
For all intents and purposes, this column is the primary key. This table has a few thousand rows, each with a unique ID. Before, the application would SELECT the MAX() value from the table, add one, then use that as the next value. This is a horrible solution, and is not transaction or thread safe (in fact, before they didn't even have a UNIQUE constraint on the column and I could see the same NOTEID was duplicated in 9 different occasions)..
I'm rather new to Oracle, so I'd like to know the best syntax to ALTER this table and make this column auto-increment instead. If possible, I'd like to make the next value in the sequence be the MAX(NOTEID) + 1 in the table, or just make it 800 or something to start out. Thanks!
You can't alter the table. Oracle doesn't support declarative auto-incrementing columns. You can create a sequence
CREATE SEQUENCE note_seq
START WITH 800
INCREMENT BY 1
CACHE 100;
Then, you can create a trigger
CREATE OR REPLACE TRIGGER populate_note_id
BEFORE INSERT ON note
FOR EACH ROW
BEGIN
:new.note_id := note_seq.nextval;
END;
or, if you want to allow callers to specify a non-default NOTE_ID
CREATE OR REPLACE TRIGGER populate_note_id
BEFORE INSERT ON note
FOR EACH ROW
BEGIN
IF( :new.note_id is null )
THEN
:new.note_id := note_seq.nextval;
END IF;
END;
If your MAX(noteid) is 799, then try:
CREATE SEQUENCE noteseq
START WITH 800
INCREMENT BY 1
Then when inserting a new record, for the NOTEID column, you would do:
noteseq.nextval