Why does constraint fail when upsert used in conjunction with triggers? - sql

Please could someone explain why this unique constraint fails and if there is any workaround or suggestions.
With this schema:
CREATE TABLE abc
(
a INTEGER NOT NULL,
b INTEGER NOT NULL,
c INTEGER NOT NULL
);
CREATE UNIQUE INDEX unique_a_b ON abc(a, b);
CREATE TABLE xyz
(
x INTEGER NOT NULL,
y INTEGER NOT NULL,
z INTEGER NOT NULL
);
CREATE UNIQUE INDEX unique_x_y ON xyz(x, y);
I have triggers on a table xyz for both insert and update operations, they both perform insert or replace on table abc which has a unique index.
CREATE TRIGGER on_insert_xyz
AFTER INSERT
ON xyz
BEGIN
INSERT OR REPLACE INTO abc(a, b, c)
SELECT * FROM xyz LIMIT 1;
END;
CREATE TRIGGER on_update_xyz
AFTER UPDATE
ON xyz
BEGIN
INSERT OR REPLACE INTO abc(a, b, c)
SELECT * FROM xyz LIMIT 1;
END;
I have an upsert statement on table xyzthat causes the triggers to execute. If the upsert statement causes the update trigger to execute, then the unique index constraint fails on table abc.
INSERT INTO xyz(x, y, z) VALUES(1, 2, 3) ON CONFLICT(x, y) DO UPDATE SET z = excluded.z -- Works;
INSERT INTO xyz(x, y, z) VALUES(1, 2, 4) ON CONFLICT(x, y) DO UPDATE SET z = excluded.z -- Fails;
UNIQUE constraint failed: abc.a, abc.b

Related

Only allow certain values in column B based on the value of column A

I want to allow allow only a set number of values to be inserted into column A, and depending on the value entered, allow only a certain number of values to be inserted into column B. For example
For example
If A = 1, B can be between 1 and 9
If A = 2, B can be between 10 AND 19
If A = 3, B can be between 20 AND 29
How can I achieve this?
I figured check constraints are the best place to start. A simple constraint will ensure only values 1-3 can be added to column A. Such as:
CREATE TABLE dbo.test (
col_a INT,
col_b INT,
CONSTRAINT ch_col_a_valid_range CHECK (col_a BETWEEN 1 AND 3)
)
GO
Then I fugured that using a scalar function to determine whether the col_b is valid, by passing in the value from col_a and col_b.
CREATE FUNCTION dbo.value_is_valid (
#a INT,
#b INT
)
RETURNS BIT
AS
BEGIN
IF (#a = 1 AND #b BETWEEN 1 AND 9) RETURN 1;
IF (#a = 2 AND #b BETWEEN 10 AND 19) RETURN 1;
IF (#a = 3 AND #b BETWEEN 20 AND 29) RETURN 1;
RETURN 0;
END
GO
Then add the constraint to the table, and call the function as part of the check.
CREATE TABLE dbo.test (
col_a INT,
col_b INT,
CONSTRAINT ch_col_a_valid_range CHECK (col_a BETWEEN 1 AND 3),
CONSTRAINT ch_col_b_valid_based_on_a CHECK(dbo.value_is_valid(col_a, col_b) = 1)
)
GO
However, the following insert fails, complaining about a conflict with the ch_col_b_valid_based_on_a constraint that was added.
INSERT INTO dbo.test (
col_a,
col_b
)
VALUES (1, 9)
The INSERT statement conflicted with the CHECK constraint "ch_col_b_valid_based_on_a". The conflict occurred in database " MyDB", table "dbo.test".
What can I do to work around this and achieve the result mentioned above?
On looking back, this approach of using a scalar function in a check constraint works exactly as expected.
One method is a check constraint:
CREATE TABLE dbo.test (
col_a INT,
col_b INT,
CONSTRAINT ch_col_a_valid_range CHECK (col_a BETWEEN 1 AND 3),
CONSTRAINT chk_col_a_colb
CHECK ( (col_a = 1 AND col_b BETWEEN 1 AND 9) OR
(col_a = 2 AND col_b BETWEEN 10 AND 19) OR
(col_a = 3 AND col_b BETWEEN 20 AND 29)
)
);
However, I might be inclined to created an AB_valid table with a list of valid pairs and use a foreign key constraint. That way, the list of valid values could be maintained dynamically rather than requiring modification to the table definition.
You could use some math to create a simpler constraint.
CREATE TABLE dbo.test (
col_a INT,
col_b INT,
CONSTRAINT ch_col_a_valid_range CHECK (col_a BETWEEN 1 AND 3),
CONSTRAINT ch_col_b_valid_based_on_a CHECK(col_b/10 + 1 = col_a)
);

Unique constraint on any combination of two columns

I am trying to implement a unique constraint for a combination of two columns. I am running oracle 11g.
Specifically I have two columns A and B.
I have a row like below
A B
1 2
Then I want the following combinations to fail when inserted
A B
1 2
2 1
Is this possible to achieve with a unique index in Oracle?
Yes, it is possible(for example using generated columns):
CREATE TABLE tab(A INT NOT NULL, B INT NOT NULL);
ALTER TABLE tab ADD c1 AS (LEAST(A,B));
ALTER TABLE tab ADD c2 AS (GREATEST(A,B));
CREATE UNIQUE INDEX UQ_tab ON tab(c1,c2);
You could hide these columns if needed(Oracle 12c):
ALTER TABLE tab MODIFY c1 INVISIBLE;
ALTER TABLE tab MODIFY c2 INVISIBLE;
DBFiddle Demo
EDIT:
Even simpler approach:
CREATE UNIQUE INDEX UQ_tab ON tab(least(A,B), greatest(A,B));
DBFiddle Demo
You can use a UNIQUE INDEX with LEAST, GREATEST and COALESCE functions:
CREATE TABLE table_name (
a INT,
b INT
);
CREATE UNIQUE INDEX table_name__a__b__u ON TABLE_NAME(
COALESCE( LEAST( a, b ), a, b ),
GREATEST( a, b )
);
You need to include COALESCE in case one of the values is NULL.
DBFiddle
INSERT INTO table_name ( a, b ) VALUES ( 1, 2 );
1 rows affected
INSERT INTO table_name ( a, b ) VALUES ( 3, NULL );
1 rows affected
INSERT INTO table_name ( a, b ) VALUES ( 2, 1 );
ORA-00001: unique constraint (SCHEMA_NAME.TABLE_NAME__A__B__U) violated
INSERT INTO table_name ( a, b ) VALUES ( NULL, 3 );
ORA-00001: unique constraint (SCHEMA_NAME.TABLE_NAME__A__B__U) violated

How to simulate group_concat in plain sql

I am using the hxtt sql driver for csv files. It only supports plain sql. is there a way to simulate group concat using plain sql statements?
How plain? If you can use triggers then you can do it fairly simply. I've used this trick before in SQLite3 when I've needed a group_concat() that allows me to specify the order in which values are to be concatenated (SQLite3 does not provide a way to do that).
So let's say we have a table like this:
CREATE TABLE t(v TEXT NOT NULL, num INTEGER NOT NULL UNIQUE);
and you want to concatenate the values of v ordered by num, with some separator character, let's say a comma.
CREATE TEMP TABLE c(v TEXT);
CREATE TEMP TABLE j(v TEXT);
CREATE TEMP TRIGGER j_ins BEFORE INSERT ON j
FOR EACH ROW
BEGIN
UPDATE c SET v = v || ',' || NEW.v;
INSERT INTO c (v) SELECT NEW.v WHERE NOT EXISTS (SELECT * FROM c);
SELECT RAISE(IGNORE);
END;
Now we can:
INSERT INTO j (c) SELECT v FROM t ORDER BY num;
SELECT v FROM c; -- this should output the concatenation of the values of v in t
DELETE FROM c;
Finally, this is a sqlite3 session showing that this works:
SQLite version 3.7.4
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> CREATE TABLE t(v TEXT NOT NULL, num INTEGER NOT NULL UNIQUE);
sqlite> CREATE TEMP TABLE c(v TEXT);
sqlite> CREATE TEMP TABLE j(v TEXT);
sqlite> CREATE TEMP TRIGGER j_ins BEFORE INSERT ON j
...> FOR EACH ROW
...> BEGIN
...> UPDATE c SET v = v || ',' || NEW.v;
...> INSERT INTO c (v) SELECT NEW.v WHERE NOT EXISTS (SELECT * FROM c);
...> SELECT RAISE(IGNORE);
...> END;
sqlite> insert into t (v, num) values (1, 0);
sqlite> insert into t (v, num) values (31, 1);
sqlite> insert into t (v, num) values (52, 2);
sqlite> insert into t (v, num) values (0, 3);
sqlite> SELECT v FROM c;
sqlite> INSERT INTO j (v) SELECT v FROM t ORDER BY num;
sqlite> SELECT v FROM c;
1,31,52,0
sqlite> SELECT v FROM j;
sqlite> DELETE FROM c;
sqlite>
Now, this isn't pure SQL, because it depends on triggers. Between recursive triggers and all the ways to do conditionals in SQL you have a Turing complete system. But if you don't have triggers, or any procedural extensions, no generator tables... then not so much.
I know nothing about hxtt, so maybe this won't help you. But SQLite3 can deal with CSV, so maybe SQLite3 can help you...

How to set two column unique in SQL

I am creating a table ,in the table two column is unique, I mean columnA and columnB do not have same value:
such as :
Table X
A B
1 2(RIGHT,unique)
2 2(RIGHT, unique)
1 3(RIGHT, not unique)
2 3(RIGHT, not unique)
1 2 (WRONG, not unique)
How to create such a table?
many thanks!
create table X
(
[ID] INTEGER PRIMARY KEY AUTOINCREASE NOT NULL,\
[A] INTEGER,
[B] INTEGER);
Create a unique key column:
CREATE TABLE X
(
ID INTEGER PRIMARY KEY AUTOINCREASE NOT NULL,
A INTEGER,
B INTEGER,
UNIQUE KEY(A, B)
);
INSERT INTO X(A, B) VALUES(1, 2);
INSERT INTO X(A, B) VALUES(2, 2);
INSERT INTO X(A, B) VALUES(1, 3);
INSERT INTO X(A, B) VALUES(2, 3);
INSERT INTO X(A, B) VALUES(1, 2);
The last line will fail because the combination a = 1 and b = 2 already exists in the table.
CREATE UNIQUE INDEX `my_index_name` ON `my_table` (`col1`,`col2`)

Calculate running total in SQLite table using triggers

How can I create a SQLite Trigger to calculate running totals on the "Actual" table? The following SQL code should update the AccountBalances table so that the Balance column counts from 1, 2, 3, ... rowcount. However, the trigger only updates the 2nd row, even when I turned on recursive_triggers. The result below is row 1 = 1, row 2 = 2, and rows after that are null.
CREATE TEMP TABLE "AccountBalances" (
"Id" INTEGER PRIMARY KEY,
"DateId" INT,
"AccountId" INT,
"AccountCurrAmount" REAL,
"Balance" REAL);
INSERT INTO "AccountBalances"
(DateId, AccountId, AccountCurrAmount)
SELECT DateId, AccountId, Sum(AccountCurrAmount)
FROM Actual
GROUP BY DateId, AccountId
ORDER BY AccountId, DateId;
CREATE TRIGGER UpdateAccountBalance AFTER UPDATE ON AccountBalances
BEGIN
UPDATE AccountBalances
SET Balance = 1 + new.Balance
WHERE Id = new.Id + 1;
END;
PRAGMA recursive_triggers = 'on';
UPDATE AccountBalances
SET Balance = 1
WHERE Id = 1
Please check the value of SQLITE_MAX_TRIGGER_DEPTH. Could it be set to 1 instead of default 1000?
Please check your SQLite version. Before 3.6.18, recursive triggers were not supported.
Please note that the following worked for me 100% OK
drop table "AccountBalances"
CREATE TEMP TABLE "AccountBalances" (
"Id" INTEGER PRIMARY KEY,
"Balance" REAL);
INSERT INTO "AccountBalances" values (1,0)
INSERT INTO "AccountBalances" values (2,0);
INSERT INTO "AccountBalances" values (3,0);
INSERT INTO "AccountBalances" values (4,0);
INSERT INTO "AccountBalances" values (5,0);
INSERT INTO "AccountBalances" values (6,0);
CREATE TRIGGER UpdateAccountBalance AFTER UPDATE ON AccountBalances
BEGIN
UPDATE AccountBalances
SET Balance = 1 + new.Balance
WHERE Id = new.Id + 1;
END;
PRAGMA recursive_triggers = 'on';
UPDATE AccountBalances
SET Balance = 1
WHERE Id = 1
select * from "AccountBalances";
Resulted in:
Id Balance
1 1
2 2
3 3
4 4
5 5
6 6