Postgres UPSERT (INSERT or UPDATE) only if value is different - sql

I'm updating a Postgres 8.4 database (from C# code) and the basic task is simple enough: either UPDATE an existing row or INSERT a new one if one doesn't exist yet. Normally I would do this:
UPDATE my_table
SET value1 = :newvalue1, ..., updated_time = now(), updated_username = 'evgeny'
WHERE criteria1 = :criteria1 AND criteria2 = :criteria2
and if 0 rows were affected then do an INSERT:
INSERT INTO my_table(criteria1, criteria2, value1, ...)
VALUES (:criteria1, :criteria2, :newvalue1, ...)
There is a slight twist, though. I don't want to change the updated_time and updated_username columns unless any of the new values are actually different from the existing values to avoid misleading users about when the data was updated.
If I was only doing an UPDATE then I could add WHERE conditions for the values as well, but that won't work here, because if the DB is already up to date the UPDATE will affect 0 rows and then I would try to INSERT.
Can anyone think of an elegant way to do this, other than SELECT, then either UPDATE or INSERT?

Take a look at a BEFORE UPDATE trigger to check and set the correct values:
CREATE OR REPLACE FUNCTION my_trigger() RETURNS TRIGGER LANGUAGE plpgsql AS
$$
BEGIN
IF OLD.content = NEW.content THEN
NEW.updated_time= OLD.updated_time; -- use the old value, not a new one.
ELSE
NEW.updated_time= NOW();
END IF;
RETURN NEW;
END;
$$;
Now you don't even have to mention the field updated_time in your UPDATE query, it will be handled by the trigger.
http://www.postgresql.org/docs/current/interactive/plpgsql-trigger.html

Two things here.
Firstly depending on activity levels in your database you may hit a race condition between checking for a record and inserting it where another process may create that record in the interim.
The manual contains an example of how to do this
link example
To avoid doing an update there is the suppress_redundant_updates_trigger() procedure. To use this as you wish you wold have to have two before update triggers the first will call the suppress_redundant_updates_trigger() to abort the update if no change made and the second to set the timestamp and username if the update is made. Triggers are fired in alphabetical order.
Doing this would also mean changing the code in the example above to try the insert first before the update.
Example of how suppress update works:
DROP TABLE sru_test;
CREATE TABLE sru_test(id integer not null primary key,
data text,
updated timestamp(3));
CREATE TRIGGER z_min_update
BEFORE UPDATE ON sru_test
FOR EACH ROW EXECUTE PROCEDURE suppress_redundant_updates_trigger();
DROP FUNCTION set_updated();
CREATE FUNCTION set_updated()
RETURNS TRIGGER
AS $$
DECLARE
BEGIN
NEW.updated := now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER zz_set_updated
BEFORE INSERT OR UPDATE ON sru_test
FOR EACH ROW EXECUTE PROCEDURE set_updated();
insert into sru_test(id,data) VALUES (1,'Data 1');
insert into sru_test(id,data) VALUES (2,'Data 2');
select * from sru_test;
update sru_test set data = 'NEW';
select * from sru_test;
update sru_test set data = 'NEW';
select * from sru_test;
update sru_test set data = 'ALTERED' where id = 1;
select * from sru_test;
update sru_test set data = 'NEW' where id = 2;
select * from sru_test;

Postgres is getting UPSERT support . It is currently in the tree since 8 May 2015 (commit):
This feature is often referred to as upsert.
This is implemented using a new infrastructure called "speculative
insertion". It is an optimistic variant of regular insertion that
first does a pre-check for existing tuples and then attempts an
insert. If a violating tuple was inserted concurrently, the
speculatively inserted tuple is deleted and a new attempt is made. If
the pre-check finds a matching tuple the alternative DO NOTHING or DO
UPDATE action is taken. If the insertion succeeds without detecting a
conflict, the tuple is deemed inserted.
A snapshot is available for download. It has not yet made a release.

INSERT INTO table_name(column_list) VALUES(value_list)
ON CONFLICT target action;
https://www.postgresqltutorial.com/postgresql-upsert/
Dummy example :
insert into user_profile (user_id, resident_card_no, last_name) values
(103, '14514367', 'joe_inserted' )
on conflict on constraint user_profile_pk do
update set resident_card_no = '14514367', last_name = 'joe_updated';

The RETURNING clause enables you to chain your queries; the second query uses the results from the first. (in this case to avoid re-touching the same rows) (RETURNING is available since postgres 8.4)
Shown here embedded in a a function, but it works for plain SQL, too
DROP SCHEMA tmp CASCADE;
CREATE SCHEMA tmp ;
SET search_path=tmp;
CREATE TABLE my_table
( updated_time timestamp NOT NULL DEFAULT now()
, updated_username varchar DEFAULT '_none_'
, criteria1 varchar NOT NULL
, criteria2 varchar NOT NULL
, value1 varchar
, value2 varchar
, PRIMARY KEY (criteria1,criteria2)
);
INSERT INTO my_table (criteria1,criteria2,value1,value2)
SELECT 'C1_' || gs::text
, 'C2_' || gs::text
, 'V1_' || gs::text
, 'V2_' || gs::text
FROM generate_series(1,10) gs
;
SELECT * FROM my_table ;
CREATE function funky(_criteria1 text,_criteria2 text, _newvalue1 text, _newvalue2 text)
RETURNS VOID
AS $funk$
WITH ins AS (
INSERT INTO my_table(criteria1, criteria2, value1, value2, updated_username)
SELECT $1, $2, $3, $4, COALESCE(current_user, 'evgeny' )
WHERE NOT EXISTS (
SELECT * FROM my_table nx
WHERE nx.criteria1 = $1 AND nx.criteria2 = $2
)
RETURNING criteria1 AS criteria1, criteria2 AS criteria2
)
UPDATE my_table upd
SET value1 = $3, value2 = $4
, updated_time = now()
, updated_username = COALESCE(current_user, 'evgeny')
WHERE 1=1
AND criteria1 = $1 AND criteria2 = $2 -- key-condition
AND (value1 <> $3 OR value2 <> $4 ) -- row must have changed
AND NOT EXISTS (
SELECT * FROM ins -- the result from the INSERT
WHERE ins.criteria1 = upd.criteria1
AND ins.criteria2 = upd.criteria2
)
;
$funk$ language sql
;
SELECT funky('AA', 'BB' , 'CC', 'DD' ); -- INSERT
SELECT funky('C1_3', 'C2_3' , 'V1_3', 'V2_3' ); -- (null) UPDATE
SELECT funky('C1_7', 'C2_7' , 'V1_7', 'V2_7777' ); -- (real) UPDATE
SELECT * FROM my_table ;
RESULT:
updated_time | updated_username | criteria1 | criteria2 | value1 | value2
----------------------------+------------------+-----------+-----------+--------+---------
2013-03-13 16:37:55.405267 | _none_ | C1_1 | C2_1 | V1_1 | V2_1
2013-03-13 16:37:55.405267 | _none_ | C1_2 | C2_2 | V1_2 | V2_2
2013-03-13 16:37:55.405267 | _none_ | C1_3 | C2_3 | V1_3 | V2_3
2013-03-13 16:37:55.405267 | _none_ | C1_4 | C2_4 | V1_4 | V2_4
2013-03-13 16:37:55.405267 | _none_ | C1_5 | C2_5 | V1_5 | V2_5
2013-03-13 16:37:55.405267 | _none_ | C1_6 | C2_6 | V1_6 | V2_6
2013-03-13 16:37:55.405267 | _none_ | C1_8 | C2_8 | V1_8 | V2_8
2013-03-13 16:37:55.405267 | _none_ | C1_9 | C2_9 | V1_9 | V2_9
2013-03-13 16:37:55.405267 | _none_ | C1_10 | C2_10 | V1_10 | V2_10
2013-03-13 16:37:55.463651 | postgres | AA | BB | CC | DD
2013-03-13 16:37:55.472783 | postgres | C1_7 | C2_7 | V1_7 | V2_7777
(11 rows)

Start a transaction. Use a select to see if the data you'd be inserting already exists, if it does, do nothing, otherwise update, if it does not exist, then insert. Finally close the transaction.

Related

Copy value from one column into another during insert using postgreSQL

If I have a table like this:
CREATE TABLE mytable
(
id SERIAL,
content TEXT,
copyofid INTEGER
);
Is there a way to copy id into copyofid in a single insert statement?
I tried:
INSERT INTO mytable(content, copyofid) VALUES("test", id);
But that doesn't seem to work.
You can find the sequence behind your serial column using pg_get_serial_sequence() and access it using currval() to get what serial column just got as a result of your INSERT.
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid INTEGER
);
--this works for a single-record insert
INSERT INTO mytable
(content, copyofid)
VALUES
('test', currval(pg_get_serial_sequence('mytable','id')));
--inserting more, you'll have to handle both columns relying on the sequence
INSERT INTO mytable
( id,
content,
copyofid)
VALUES
( nextval(pg_get_serial_sequence('mytable','id')),
'test3',
currval(pg_get_serial_sequence('mytable','id'))),
( nextval(pg_get_serial_sequence('mytable','id')),
'test4',
currval(pg_get_serial_sequence('mytable','id')));
table mytable;
-- id | content | copyofid
------+---------+----------
-- 1 | test | 1
-- 2 | test3 | 2
-- 3 | test4 | 3
--(3 rows)
Fiddle
Edouard makes makes a fair point that if you can specify the conditions when you want this behaviour, you can add them to the definition:
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when content ilike '%requires copying ID%' then id end)
stored
);
insert into mytable (content) values ('abc') returning *;
-- id | content | copyofid
------+---------+----------
-- 1 | abc |
--(1 row)
insert into mytable (content) values ('abc, but requires copying ID') returning *;
-- id | content | copyofid
------+------------------------------+----------
-- 2 | abc, but requires copying ID | 2
--(1 row)
If they vary between inserts
CREATE TABLE mytable
( id SERIAL,
content TEXT,
copyofid integer
generated always as (
case when should_copy_id then id end)
stored,
should_copy_id boolean default false
);
insert into mytable (content) values ('efg') returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 1 | efg | | f
--(1 row)
insert into mytable (content,should_copy_id) values ('klm','today'::date<>'2022-10-28'::date) returning *;
-- id | content | copyofid | should_copy_id
------+---------+----------+----------------
-- 2 | klm | 2 | t
--(1 row)
The trigger will be better if
the check is fairly complex - generated columns are pretty limited in terms of the definition complexity. For example, you can't use mutable functions in them - not even STABLE are accepted
you want to save the logic and change it later without having to drop the column each time, then re-add it with a new definition (only way to alter a generated column definition)
as a part of the insert you'll want to do more than just copy the id column
The solution is to create a trigger function which is fired before inserting a new row in table mytable and which copy NEW.id into NEW.copyofid if a condition is true :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
IF condition
THEN NEW.copyofid = NEW.id ;
END IF ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
The condition can also be stated directly in the WHEN clause of the trigger instead of in the function :
CREATE OR REPLACE FUNCTION before_insert_mytable() RETURN trigger LANGUAGE plpgsql AS $$
BEGIN
NEW.copyofid = NEW.id ;
RETURN NEW ;
END ; $$
CREATE OR REPLACE TRIGGER before_insert_mytable BEFORE INSERT ON mytable
WHEN condition
FOR EACH ROW EXECUTE FUNCTION before_insert_mytable () ;
see the manual

In Oracle, I want to use a sequence and not allow Insert on the column that uses the sequence

I want to make happen the same that happens when I do the following
CREATE TABLE "TEST1"
(
"ID" NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
"APPCODE" VARCHAR2(1)
);
Table TEST1 created.
INSERT INTO TEST1 (ID, APPCODE) VALUES (1,'A');
Error starting at line : 6 in command -
INSERT INTO TEST1 (ID, APPCODE) VALUES (1,'A')
Error at Command Line : 50 Column : 1
Error report -
SQL Error: ORA-32795: cannot insert into a generated always identity column
INSERT INTO TEST (APPCODE) VALUES ('A');
1 row inserted.
but I want to use named sequences, created by me. I want the same behavior as
when using the "ALWAYS" keyword (as in "GENERATED ALWAYS AS IDENTITY") and at the same time use my own named sequences, but I don't know how.
With named sequences, it seems to be impossible to avoid that an INSERT uses the ID COLUMN on the insert. But maybe there is a way? This is the question I'm asking. Below I create a named sequence and show the difference (I can't figure out how to prevent the ID column to be allowed on the insert).
CREATE SEQUENCE SEQ_TEST2 START WITH 1 INCREMENT BY 1 MINVALUE 1 NOMAXVALUE;
Sequence SEQ_TEST2 created.
INSERT INTO TEST2 (APPCODE) VALUES ('A'); /* This is ok */
1 row inserted.
INSERT INTO TEST2 (ID,APPCODE) VALUES (1928,'A'); /* This is NOT ok */
1 row inserted.
The second insert above is what I want to prevent from happening, it shouldn't be possible to insert on the ID column. I don't care how to prevent it to happen, doesn't have to be the same way that the "ALWAYS" keyword on the TEST1 table works, but I would like to prevent it from happening. Anyone knows please how to to it?
When you define a column as a identity column, Oracle automatically creates a sequence, you just don't get to choose the name. You can view the name of the sequence that was created and will be used to populate the identity in the DATA_DEFAULT column of the ALL_TAB_COLS table.
SELECT owner,
table_name,
column_name,
data_default
FROM all_tab_cols
WHERE identity_column = 'YES';
Why do you thing that while using IDENTITY you do not use a SEQUENCE?
Check the documentation or the example below
CREATE TABLE "TEST1"
(
"ID" NUMBER(10,0) GENERATED ALWAYS AS IDENTITY,
"APPCODE" VARCHAR2(1)
);
For this table Oracle creates a sequence for you under the over:
EXPLAIN PLAN SET STATEMENT_ID = 'jara1' into plan_table FOR
insert into TEST1 (APPCODE) values ('x');
---
SELECT * FROM table(DBMS_XPLAN.DISPLAY('plan_table', 'jara1','ALL'));
-----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------
| 0 | INSERT STATEMENT | | 1 | 100 | 1 (0)| 00:00:01 |
| 1 | LOAD TABLE CONVENTIONAL | TEST1 | | | | |
| 2 | SEQUENCE | ISEQ$$_75209 | | | | |
-----------------------------------------------------------------------------------------
Or check the dictionary
select SEQUENCE_NAME from USER_TAB_IDENTITY_COLS
where table_name = 'TEST1';
SEQUENCE_NAME
---------------
ISEQ$$_75209
In identity_options you can define the sequence options.
By selection ALWAYS or BY DEAFULT [ON NULL] you can adjust what is posible / now allowed to use in insert (I'm not sure from you description what is your aim).

Returning a column from an INSERTED record via an updateable view

I have a view I need to allow my users to update and insert on. Importantly, when they Insert, they need to be able to return the new value from the Inserted row, however right now they get NULL. It must be a view, as in the SELECT of the view, it needs to be able to return values that are the result of a join.
My underlying table definitions:
CREATE TABLE my_assets (
asset_id bigserial not null primary key,
asset_price NUMERIC(32,10) -- This will vary constantly via an independent process
);
CREATE TABLE my_transactions (
id bigserial not null primary key,
asset_id bigint not null REFERENCES my_assets(asset_id),
some_text varchar(100)
);
INSERT INTO my_assets(asset_price) SELECT 100 as asset_price;
My view that shows the result of the table:
CREATE VIEW my_transactions_view AS
SELECT tx.id, tx.asset_id, tx.some_text, a.asset_price
FROM my_transactions tx
JOIN my_assets a ON tx.asset_id = a.asset_id
My trigger that allows insertion into my_transactions_view:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
SELECT NEW.asset_id, NEW.some_text;
RETURN NEW;
END
$BODY$
LANGUAGE 'plpgsql';
CREATE TRIGGER trig_my_transactions_view INSTEAD OF INSERT on my_transactions_view
FOR EACH ROW EXECUTE PROCEDURE trigfx_insert_to_my_transactions_view();
All good so far. However, the problem arises from trying to run the below SQL:
INSERT INTO my_transactions_view(asset_id, some_text)
SELECT 1 as asset_id, 'Hello World' as some_text
RETURNING id, asset_id, some_text;
The returned table returns NULL for ID, but I want to return the newly updated ID from the my_transactions table:
|---------------------|------------------|------------------|
| ID | asset_id | some_text |
|---------------------|------------------|------------------|
| null | 1 | Hello World |
|---------------------|------------------|------------------|
Running a subsequent SELECT * FROM my_transactions_view DOES produce the updated result:
|------------------|------------------|------------------|------------------|
| ID | asset_id | some_text | asset_price |
|------------------|------------------|------------------|------------------|
| 1 | 1 | Hello World | 100.0000000 |
|------------------|------------------|------------------|------------------|
but I need it produced during the RETURNING of the INSERT statement.
Thank you!!!
You can populate the new record with the generated ID:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text);
new.id := lastval(); --<< this gets the generated id from the transactions table
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
Online example
Alternatively you could use currval(pg_get_serial_sequence('my_transactions','id')) instead of lastval()
Turns out we can avoid the extra function call via SELECT INTO from a CTE:
CREATE OR REPLACE FUNCTION trigfx_insert_to_my_transactions_view()
RETURNS trigger AS
$BODY$
BEGIN
WITH ins_q as (INSERT INTO my_transactions(asset_id, some_text)
values (NEW.asset_id, NEW.some_text)
RETURNING id, asset_id, some_text)
SELECT ins_q.id, ins_q.asset_id, ins_q.some_text
INTO NEW.id, NEW.asset_id, NEW.some_text
FROM ins_q;
RETURN NEW;
END
$BODY$
LANGUAGE plpgsql;
See online example here.
I was running into initialisation errors (lastval is not yet defined in this session) with the new.id := lastval(); approach.

How to set a value in a variable column name?

How do I set a value in a variable column name? For some context, I am writing a function to be used as a trigger which sets a variable column to a constant value. To be used as follows:
CREATE TRIGGER always_6_trigger
BEFORE INSERT
ON table
FOR EACH ROW
EXECUTE PROCEDURE always_6('col1');
The above would result in the following rows all having a col1 value of 6. So for example:
INSERT INTO table (col1, col2) VALUES (6, 2), (null, 9), (null, 10), (7, 2);
Would result in:
| col1 | col2 |
---------------
| 6 | 2 |
| 6 | 9 |
| 6 | 10 |
| 6 | 2 |
Or if using the following trigger:
CREATE TRIGGER always_6_trigger
BEFORE INSERT
ON table
FOR EACH ROW
EXECUTE PROCEDURE always_6('col2');
And the same insert:
INSERT INTO table (col1, col2) VALUES (6, 2), (null, 9), (null, 10), (7, 2);
The table would look like:
| col1 | col2 |
---------------
| 6 | 6 |
| null | 6 |
| null | 6 |
| 7 | 6 |
How would I write the always_6 function?
Edit: To better explain the use case, the constant value would be current_setting('user_id') (or something alike). And the column name would be things like author_id and user_id. The thinking being that a user could never add for data which was not their own.
You can define your function to produce dynamically generated SQL.
The EXECUTE command takes a string as input and executes it as SQL, so it would look something like this:
EXECUTE FORMAT('UPDATE mytable SET %I='constantvalue' WHERE condition', colname);
Here I have used the FORMAT function to prepare a string with the value of colname substituted in where the column name would go. condition would be some valid WHERE clauses to select the record to update.
If the value of colname could come from an external source (ie. user supplied data) then you would have to be very careful to validate it beforehand, otherwise you might create an SQL injection vector.
You can use a dynamic query to do this with conditional check on the passed parameter value to the input parameter of the stored procedure.
I think I oversimplified the task based on the initial description, but would something like this work? You can't pass a parameter to a trigger function, but you mentioned the parameter was the result of another function current_setting('user_id'), so is it possible to roll the two concepts together like this?
CREATE OR REPLACE FUNCTION always_6()
RETURNS trigger AS
$BODY$
DECLARE
current_user_id varchar;
BEGIN
current_user_id := current_setting('user_id');
if current_user_id = 'test1' then
new.col_1 := 6;
elsif current_user_id = 'test2' then
new.col_2 := 6;
end if;
return NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

mysql procedure to update numeric reference in previous rows when one is updated

There's a table like this one
______________________
| id | title | order |
|----------------------|
| 1 | test1 | 1 |
|-----|--------|-------|
| 2 | test2 | 2 |
|-----|--------|-------|
| 3 | test3 | 3 |
|-----|--------|-------|
| 4 | test4 | 4 |
'----------------------'
when i introduce in my mysql shell a single update to a row
$sql> UPDATE `table` SET order=1 WHERE id=3;
And then procedure or method resamples order column in the before update lower values to get its order renewed like this
______________________
| id | title | order |
|----------------------|
| 1 | test1 | 2 |
|-----|--------|-------|
| 2 | test2 | 3 |
|-----|--------|-------|
| 3 | test3 | 1 |
|-----|--------|-------|
| 4 | test4 | 4 |
'----------------------'
Any help would be appreciated, thanks!
There are two cases to consider, I think:
Move one row so it appears earlier in the ordering.
Move one row so it appears later in the ordering.
It is non-trivial either way. It is not clear whether there is a unique constraint on the column 'order'; the end result is certainly supposed to have a unique ordering.
Notation:
'On' refers to the row with value 'order = n' in the old values
'Nn' refers to the row with 'order = n' in the new values
In the example (illustrative of case 1):
O3 --> N1
O1 --> N2
O2 --> N3
As an alternative, consider moving id = 2 so it has order = 4:
O2 --> N4
O3 --> N2
O4 --> N3
You are basically adding or subtracting one from the 'other' rows, where those are the rows in the old order between the old position of the moved row and the new position of the moved row. In a pseudo-code, using $old and $new to identify the before and after positions of the moved row, and dealing with case 1 ($old > $new):
UPDATE AnonymousTable
SET order = CASE
WHEN order = $old THEN $new
WHEN order >= $new AND order < $old THEN order + 1
END CASE
WHERE order BETWEEN $new AND $old;
The corresponding code for case 2 ($old < $new) is:
UPDATE AnonymousTable
SET order = CASE
WHEN order = $old THEN $new
WHEN order > $new AND order <= $old THEN order - 1
END CASE
WHERE order BETWEEN $old AND $new;
Given the WHERE clause on the UPDATE as a whole, you may be able to remove the second WHEN in the CASE and replace it with a simple ELSE.
UPDATE AnonymousTable
SET order = CASE
WHEN order = $old THEN $new
ELSE order + 1
END CASE
WHERE order BETWEEN $new AND $old;
UPDATE AnonymousTable
SET order = CASE
WHEN order = $old THEN $new
ELSE order - 1
END CASE
WHERE order BETWEEN $old AND $new;
I think a stored procedure is in order - choosing between the two statements based on the input parameters $old, $new. You might be able to do something with a judicious mix of expressions such as '($old - $new) / ABS($old - $new)' and 'MIN($old, $new)' and 'MAX($old, $new)' where the MIN/MAX are not aggregates but comparator functions for a pair of values (as found in Fortran, amongst other programming languages).
Note that I am assuming that while a single SQL statement is executing, the uniqueness constraint (if any) is not enforced as each row is changed - only when the statement completes. This is necessary since you can't actually control the order in which the rows are processed. I know of DBMS where this would cause trouble; I know of others where it would not.
It can all be done in a single SQL statement - but you do want a stored procedure to sort out the parameters to the statement. I use IBM Informix Dynamic Server (11.50.FC6 on MacOS X 10.6.2), and that is one of the DBMS that enforces the unique constraint on the 'order' column at the end of the statement. I did the development of the SQL without the UNIQUE constraint; that worked too, of course. (And yes, IDS does allow you to roll back DDL statements like CREATE TABLE and CREATE PROCEDURE. What did you say? Your DBMS doesn't? How quaint!)
BEGIN WORK;
CREATE TABLE AnonymousTable
(
id INTEGER NOT NULL PRIMARY KEY,
title VARCHAR(10) NOT NULL,
order INTEGER NOT NULL UNIQUE
);
INSERT INTO AnonymousTable VALUES(1, 'test1', 1);
INSERT INTO AnonymousTable VALUES(2, 'test2', 2);
INSERT INTO AnonymousTable VALUES(3, 'test3', 3);
INSERT INTO AnonymousTable VALUES(4, 'test4', 4);
SELECT * FROM AnonymousTable ORDER BY order;
CREATE PROCEDURE move_old_to_new(old INTEGER, new INTEGER)
DEFINE v_min, v_max, v_gap, v_inc INTEGER;
IF old = new OR old IS NULL OR new IS NULL THEN
RETURN;
END IF;
LET v_min = old;
IF new < old THEN
LET v_min = new;
END IF;
LET v_max = old;
IF new > old THEN
LET v_max = new;
END IF;
LET v_gap = v_max - v_min + 1;
LET v_inc = (old - new) / (v_max - v_min);
UPDATE AnonymousTable
SET order = v_min + MOD(order - v_min + v_inc + v_gap, v_gap)
WHERE order BETWEEN v_min AND v_max;
END PROCEDURE;
EXECUTE PROCEDURE move_old_to_new(3,1);
SELECT * FROM AnonymousTable ORDER BY order;
EXECUTE PROCEDURE move_old_to_new(1,3);
SELECT * FROM AnonymousTable ORDER BY order;
INSERT INTO AnonymousTable VALUES(5, 'test5', 5);
INSERT INTO AnonymousTable VALUES(6, 'test6', 6);
INSERT INTO AnonymousTable VALUES(7, 'test7', 7);
INSERT INTO AnonymousTable VALUES(8, 'test8', 8);
EXECUTE PROCEDURE move_old_to_new(3,6);
SELECT * FROM AnonymousTable ORDER BY order;
EXECUTE PROCEDURE move_old_to_new(6,3);
SELECT * FROM AnonymousTable ORDER BY order;
EXECUTE PROCEDURE move_old_to_new(7,2);
SELECT * FROM AnonymousTable ORDER BY order;
EXECUTE PROCEDURE move_old_to_new(2,7);
SELECT * FROM AnonymousTable ORDER BY order;
ROLLBACK WORK;
The pairs of invocations of the stored procedure with the numbers reversed reinstated the original order each time. Clearly, I could redefine the v_inc variable so that instead of being just ±1, it was 'LET v_inc = v_inc - v_min + v_gap;' and then the MOD expression would be just 'MOD(order + v_inc, v_gap)'. I've not checked whether this works with negative numbers.
Adaptation to MySQL or other DBMS is left as an exercise for the reader.
A different approach is to use floating-point numbers instead of integers for the sorting. In this setup, you need to update only a single row when changing the sorting. Let's start with this:
id order
1 1
2 2
3 3
4 4
Now you want to change the order so that item 2 appears between item 3 and 4. All you have to do is update item 2 so that its new order is a value between 3 and 4, for example 3.5:
id order
1 1
2 3.5
3 3
4 4
I guess what you are trying to achieve is better done not with a DB query, but with a simple sorting construct.
You should save your db records as objects in a collection or structure/array/list of some kind, it depends on your language of choice.
Then you create a simple sorting algorithm, sort all records based on order modifying all orders of the other lines and create a procedure that updates all of the modified lines automatically after passing it the same collection.
Maybe do 2 update statements:
UPDATE `table` SET ord=ord+1 WHERE ord >= 1 order by ord desc;
UPDATE `table` SET ord=1 WHERE id=3;
(probably want to group those two operations into a single transaction instead of using autocommit)
EDIT: add "order by" to the first update to control the update order, avoiding the "duplicate key problem. (also, avoiding the "order" keyword in column names :-)
psuedo code:
CREATE TRIGGER reorder AFTER UPDATE ON `table`
FOR EACH ROW BEGIN
UPDATE `table` SET order=order+1 WHERE id < 3 ORDER BY id DESC LIMIT 1;
END;
|
delimiter ;
Previous Id:
UPDATE `table` SET order=order+1 WHERE id < 3 ORDER BY id DESC LIMIT 1;
Assuming you have passed in the id of the row to change as change_row_id and the new order as new_order:
current_order = SELECT order from 'table' WHERE id=change_row_id;
if (current_order > new_order)
UPDATE `table` SET order=order+1 WHERE (order > new_order AND
order < current_order);
else
[you'll have to figure out how you want to handle this. Do you want the
orders to all be sequential with no breaks?]
ENDYIF;
UPDATE 'table' SET order=new_order WHERE id=change_row_id;
I don't know mysql, so you may need to tweak the sql. And you definitely want to do this in a single transaction. Either commit everything or nothing.
For all who has the same problem as the threadstarter but doesn't use a IDS as DBMS like #Jonathan Leffler:
Here is a pastebin from these procedure for MySQL
http://pastebin.com/AxkJQmAH