Postgres - upsert on passed parameter - sql

In my project I am using Postgres 12 and I want to use one sql query to INSERT OR UPDATE..
My syntax is not correct.
UPDATE: Insert works but updating does not.
ERROR: Invalid parameter number: :name"
'INSERT INTO user (
name, url
) VALUES (:name, :url)
ON CONFLICT (id)
WHERE id = :userId
DO UPDATE SET
name = :name,
url = :url'
I am using this EXAMPLE to do UPSERT and I want to UPDATE if userId is passed and if not to INSERT new row.
Thanks

BEGIN;
CREATE TABLE users (
user_id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
name text,
url text
);
INSERT INTO users (name, url)
VALUES ('Hello', 'world');
COMMIT;
using psql: https://www.postgresql.org/docs/current/app-psql.html
set variable in psql: How do you use script variables in psql?
You can also set variable in an transaction.
BEGIN;
\set name 'hi'
\set url 'yech'
INSERT INTO users (user_id, name, url)
VALUES (1, :'name', :'url')
ON CONFLICT (user_id)
DO UPDATE SET
name = EXCLUDED.name, url = EXCLUDED.url
RETURNING
*;
TABLE users;
COMMIT;

Related

The following query use for check duplicate data in table then update or insert row

I have the following query use for check duplicate data in table. If match data then update row else insert new row. In my case I have already one matched row in att_log table where emp_id=19.1.0121 and where mp_pk_id='32' AND att_date='2021-10-01', so result should be SET holiday=H in the matched row. But the DECLARE statement run without error and in console show affected row:1, but no change occur in data base, holiday not set to "H".
DECLARE c_emp_id att_log.emp_id%type;
BEGIN
SELECT emp_id
INTO c_emp_id
FROM att_log
WHERE emp_id='19.1.0121'
AND emp_pk_id='32'
AND att_date='2021-10-01' ;
EXCEPTION
WHEN TOO_MANY_ROWS THEN
UPDATE att_log
SET holiday = 'H',
updated_at = '2021-08-22'
WHERE emp_id='19.1.0121'
AND att_date='2021-10-01';
WHEN NO_DATA_FOUND THEN
INSERT INTO att_log (emp_id, emp_pk_id, att_date, holiday,login_time, logout_time)
VALUES ('19.1.0121', '32', '2021-10-01','H','','');
COMMIT WORK;
END;
If I run the query separately without DECLARE statement then data row change happen, but with the above DECLARE statement no change happen in data row in the ORACLE table. What is my fault! Sorry, I am new to ORACLE, and also sorry for poor English.
A MERGE operation can INSERT or UPDATE (and also DELETE) depending on whether the row exists or not.
Here's a working test case:
Test case / fiddle
Example of MERGE:
CREATE TABLE logs (
id NUMBER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL
, text VARCHAR2(20) UNIQUE
, q int DEFAULT 1
);
INSERT INTO logs (text) VALUES ('A');
INSERT INTO logs (text) VALUES ('B');
INSERT INTO logs (text) VALUES ('C');
MERGE INTO logs USING (SELECT 'B' AS text FROM dual) cte ON (cte.text = logs.text)
WHEN MATCHED THEN UPDATE SET logs.q = logs.q + 1
WHEN NOT MATCHED THEN INSERT (logs.text)
VALUES (cte.text)
;
Result:
and now we can do this for several existing rows and new rows at once:
MERGE INTO logs USING (
SELECT text FROM logs WHERE text > 'A' UNION
SELECT 'Z' FROM dual
) cte ON (cte.text = logs.text)
WHEN MATCHED THEN UPDATE SET logs.q = logs.q + 1
WHEN NOT MATCHED THEN INSERT (logs.text)
VALUES (cte.text)
;
New Result:
This example will either INSERT a new row when the rows in cte do not exist in logs, and UPDATE any existing rows in logs when matches are found, by incrementing q.

Update table column with primary key after insert (in one statement)

Using an update statement, how can i directly set a column value to the primary key of an insert statement? I need this as one statement.
I know the following is wrong, but it helps get my idea through:
update AppNationalities
set CountrySelectionID = (
select [INSERTED.pkID] from
(
insert into CountrySelections
output INSERTED.pkID
values(CountryID, 'test', 0)
)
)
Put the values into a temporary table:
declare #ids table (pkID int);
insert into CountrySelections
output INSERTED.pkID into #ids
values (CountryID, 'test', 0);
update AppNationalities
set CountrySelectionID = i.id
from #ids i;
I don't think the output clause is allowed in the FROM clause of an UPDATE or DELETE statement.

Bulk insert, update if on conflict (bulk upsert) on Postgres

I am writing a data-mining program, which bulk inserts user data.
The current SQL is just a plain bulk insert:
insert into USERS(
id, username, profile_picture)
select unnest(array['12345']),
unnest(array['Peter']),
unnest(array['someURL']),
on conflict (id) do nothing;
How do I do an update if on conflict? I tried:
...
unnest(array['Peter']) as a,
unnest(array['someURL']) as b,
on conflict (id) do
update set
username = a,
profile_picture = b;
But it throws There is a column named "a" in table "*SELECT*", but it cannot be referenced from this part of the query. error.
EDIT:
Table of USERS is very simple:
create table USERS (
id text not null primary key,
username text,
profile_picture text
);
Turns out a special table named excluded contains the row-to-be-inserted
(strange name though)
insert into USERS(
id, username, profile_picture)
select unnest(array['12345']),
unnest(array['Peter']),
unnest(array['someURL'])
on conflict (id) do
update set
username = excluded.username,
profile_picture = excluded.profile_picture;
http://www.postgresql.org/docs/9.5/static/sql-insert.html#SQL-ON-CONFLICT
The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to rows proposed for insertion using the special excluded table...
For bulk insert from another table if they are identical you can do it like that :
INSERT INTO table_a (SELECT * FROM table_b)
ON CONFLICT ON CONSTRAINT "pk_guid"
DO UPDATE SET
column1 = excluded.column1,
column2 = excluded.column2,
column3 = excluded.column3,
...... ;

how to disable some column using insert trigger?

I have a table with 5 columns:
username (varchar)
password (int)
access (bit)
information (varchar)
image (varchar)
I want to prevent user from inserting to 2 columns information and image if access = true.
Is there anyway to do this using insert trigger ? Any help would be great.
With an INSTEAD OF INSERT trigger, you could easily "filter out" unwanted information, e.g. you could insert an empty string (or something else) in case that access is set to 1:
CREATE TRIGGER InsteadTrigger on dbo.YourTableNameHere
INSTEAD OF INSERT
AS
BEGIN
INSERT INTO dbo.YourTableNameHere(username, password, access, information, image)
SELECT
username, password, access,
CASE access
WHEN 1 THEN '' ELSE i.information END,
CASE access
WHEN 1 THEN '' ELSE i.image END
FROM INSERTED i
END;
So if you insert a row with access = 0 - all the columns get stored as presented.
So if you try to insert a row with access = 1 - the columns information and image are being "cleared out" and an empty string is stored instead.
On SQL Server 2008 and newer, this insert here:
INSERT INTO dbo.YourTableNameHere(username, password,access,information, image)
VALUES ('test 1', 42, 0, 'testinfo 1', 'testimg 1'),
('test 2', 4711, 1, 'testinfo 2', 'testimg2')
SELECT * FROM dbo.YourTableNameHere
would result in two rows being saved to your database table, but the second row inserted will have empty information and image columns...
A simple CHECK constraint could be enough if you need this behavior at insert or update:
ALTER TABLE MySchema.MyTable
ADD CONSTRAINT CK_MyTable_BlockInformationImageWhenAccessIsTrue
CHECK( access = 1 AND information IS NULL AND image IS NULL OR access = 0 );
If you need this behavior only at insert moment then you could use this trigger:
CREATE TRIGGER trgI_MyTable_BlockInformationImageWhenAccessIsTrue
ON MySchema.MyTable
AFTER INSERT
AS
BEGIN
IF EXISTS
(
SELECT *
FROM inserted i
WHERE i.access = 1
AND (information IS NOT NULL OR image IS NOT NULL)
)
BEGIN
ROLLBACK TRANSACTION;
RAISERROR('Access denied', 16, 1);
END
END;
GO

Can you access the auto increment value in MySQL within one statement?

I have a MySQL database which contains a table of users. The primary key of the table is 'userid', which is set to be an auto increment field.
What I'd like to do is when I insert a new user into the table is to use the same value that the auto increment is creating in the 'userid' field in a different field, 'default_assignment'.
e.g.
I'd like a statement like this:
INSERT INTO users ('username','default_assignment') VALUES ('barry', value_of_auto_increment_field())
so I create user 'Barry', the 'userid' is generated as being 16 (for example), but I also want the 'default_assignment' to have the same value of 16.
Is there any way to achieve this please?
Thanks!
Update:
Thanks for the replies. The default_assignment field isn't redundant. The default_assigment can reference any user within the users table. When creating a user I already have a form that allows a selection of another user as the default_assignment, however there are cases where it needs to be set to the same user, hence my question.
Update:
Ok, I've tried out the update triggers suggestion but still can't get this to work. Here's the trigger I've created:
CREATE TRIGGER default_assignment_self BEFORE INSERT ON `users`
FOR EACH ROW BEGIN
SET NEW.default_assignment = NEW.userid;
END;
When inserting a new user however the default_assignment is always set to 0.
If I manually set the userid then the default_assignment does get set to the userid.
Therefore the auto assignment generation process clearly happens after the trigger takes effect.
there's no need to create another table, and max() will have problems acording to the auto_increment value of the table, do this:
CREATE TRIGGER trigger_name BEFORE INSERT ON tbl FOR EACH ROW
BEGIN
DECLARE next_id;
SET next_id = (SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='tbl');
SET NEW.field = next_id;
END
I declare the next_id variable because usually it will be used in some other way(*), but you could do straight new.field=(select ...)
CREATE TRIGGER trigger_name BEFORE INSERT ON tbl FOR EACH ROW
BEGIN
SET NEW.field=(SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='tbl');
END
Also in cases of (SELECT string field) you can use CAST value;
CREATE TRIGGER trigger_name BEFORE INSERT ON tbl FOR EACH ROW
BEGIN
SET NEW.field=CAST((SELECT aStringField FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='tbl') AS UNSIGNED);
END
(*) To auto-name an image:
SET NEW.field = CONCAT('image_', next_id, '.gif');
(*) To create a hash:
SET NEW.field = CONCAT( MD5( next_id ) , MD5( FLOOR( RAND( ) *10000000 ) ) );
try this
INSERT INTO users (default_assignment) VALUES (LAST_INSERT_ID()+1);
seeing that last_insert_id() wouldn't work in this case, yes, the trigger would be the only way to accomplish that.
I do ask myself though: What do you need this functionality for? Why do you store the users id twice? Personally, I don't like storing redundant data like this and I'd probably solve this in application code by making that ominous default_assignment column NULL and using the user id in my application code if default_assignment was NULL.
Actually I just tried to do the same thing as was suggested above. But it seems Mysql doesent generate the inserted ID before the row actually gets commited. So NEW.userid will always return 0 in a Before insert trigger.
The above also wont work unless it is a BEFORE INSERT trigger, since you cant update values in a AFTER INSERT query.
From a Mysql Forum Post It seems the only way to handle this is using an additional table as a sequence. So that your trigger can pull in the values from an external source.
CREATE TABLE `lritstsequence` (
`idsequence` int(11) NOT NULL auto_increment,
PRIMARY KEY (`idsequence`)
) ENGINE=InnoDB;
CREATE TABLE `lritst` (
`id` int(10) unsigned NOT NULL auto_increment,
`bp_nr` decimal(10,0) default '0',
`descr` varchar(128) default NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `dir1` (`bp_nr`)
) ENGINE=InnoDB;
DELIMITER $$
DROP TRIGGER /*!50032 IF EXISTS */ `lritst_bi_set_bp_nr`$$
CREATE TRIGGER `lritst_bi_set_bp_nr` BEFORE INSERT ON `lritst`
FOR EACH ROW
BEGIN
DECLARE secuencia INT;
INSERT INTO lritstsequence (idsequence) VALUES (NULL);
SET secuencia = LAST_INSERT_ID();
SET NEW.id = secuencia;
SET NEW.bp_nr = secuencia;
END;$$
DELIMITER ;
INSERT INTO lritst (descr) VALUES ('test1');
INSERT INTO lritst (descr) VALUES ('test2');
INSERT INTO lritst (descr) VALUES ('test3');
SELECT * FROM lritst;
Result:
id bp_nr descr
------ ------ ------
1 1 test1
2 2 test2
3 3 test3
This was copied from forums.mysql.com/read.php?99,186171,186241#msg-186241 but Im not allowed to post links yet.
The only I found that would solve this problem without an extra table would be to calculate self the next number and put that in the fields required.
CREATE TABLE `Temp` (
`id` int(11) NOT NULL auto_increment,
`value` varchar(255) ,
PRIMARY KEY (`idsequence`)
) ENGINE=InnoDB;
CREATE TRIGGER temp_before_insert BEFORE INSERT ON `Temp`
FOR EACH ROW
BEGIN
DECLARE m INT;
SELECT IFNULL(MAX(id), 0) + 1 INTO m FROM Temp;
SET NEW.value = m;
-- NOT NEEDED but to be safe that no other record can be inserted in the meanwhile
SET NEW.id = m;
END;
basically, the solution is like Resegue said.
But if you want it in one statement, you will use one of the below ways:
1. One long statement:
INSERT INTO `t_name`(field) VALUES((SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t_name'))
or for text with number:
INSERT INTO `t_name`(field) VALUES(CONCAT('Item No. ',CONVERT((SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='t_name') USING utf8)))
it looks more clearly in PHP:
$pre_name='Item No. ';
$auto_inc_id_qry = "(SELECT AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='$table')";
$new_name_qry = "CONCAT('$pre_name',CONVERT($auto_inc_id_qry USING utf8))";
mysql_query("INSERT INTO `$table`(title) VALUES($new_name_qry)");
2. Using function: (not tested yet)
CREATE FUNCTION next_auto_inc(table TINYTEXT) RETURNS INT
BEGIN
DECLARE next_id INT;
SELECT AUTO_INCREMENT FROM information_schema.TABLES
WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=table INTO next_id;
RETURN next_id;
END
INSERT INTO users ('username','default_assignment')
VALUES ('barry', next_auto_inc('users'))
$ret = $mysqli->query("SELECT Auto_increment FROM information_schema.tables WHERE table_schema = DATABASE() ");l
while ($row = mysqli_fetch_array($ret)) {
$user_id=$row['Auto_increment'];
}
You can do this reliably using a simple subquery:
INSERT INTO users ('username','default_assignment')
SELECT 'barry', Auto_increment FROM information_schema.tables WHERE TABLE_NAME='users'
I tested the above trigger idea with 10 concurrent threads doing inserts and I got over 1000 cases of 2 or 3 duplicates after ~25k inserted.
DROP TABLE IF EXISTS test_table CASCADE;
CREATE TABLE `test_table` (
`id` INT NOT NULL AUTO_INCREMENT,
`update_me` VARCHAR(36),
`otherdata` VARCHAR(36) NOT NULL,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8
COMMENT 'test table for trigger testing';
delimiter $$
DROP TRIGGER IF EXISTS setnum_test_table;
$$
CREATE TRIGGER setnum_test_table
BEFORE INSERT ON test_table FOR EACH ROW
-- SET OLD.update_me = CONCAT(NEW.id, 'xyz');
BEGIN
DECLARE next_id INT;
SET next_id = (SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='test_table' LOCK IN SHARE MODE );
-- SET NEW.update_me = CONCAT(next_id, 'qrst');
SET NEW.update_me = next_id;
END
$$
delimiter ;
-- SELECT AUTO_INCREMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='test_table'
INSERT INTO test_table (otherdata) VALUES ('hi mom2');
SELECT count(*) FROM test_table;
SELECT * FROM test_table;
-- select count(*) from (
select * from (
SELECT count(*) as cnt ,update_me FROM test_table group by update_me) q1
where cnt > 1
order by cnt desc
I used 10 of:
while true ; do echo "INSERT INTO test_table (otherdata) VALUES ('hi mom2');" | mysql --user xyz testdb ; done &
And ran the last query to watch for duplicates
example output:
'3', '4217'
'3', '13491'
'2', '10037'
'2', '14658'
'2', '5080'
'2', '14201'
...
Note 'LOCK IN SHARE MODE' didn't change anything. With and without gave duplicates at about the same rate. It seems that MySQL AUTO_INCREMENT doesn't work like Postgres' next_val() and is NOT concurrency safe.
I know this post is from 2010, but I couldn't find a good solution.
I've solved this by creating a separate table that holds the counters. When I need to generate an unique identifier for a column I just call a Stored proc:
CREATE DEFINER=`root`#`localhost` PROCEDURE `IncrementCounter`(in id varchar(255))
BEGIN
declare x int;
-- begin;
start transaction;
-- Get the last counter (=teller) and mark record for update.
select Counter+1 from tabel.Counter where CounterId=id into x for update;
-- check if given counter exists and increment value, otherwise create it.
if x is null then
set x = 1;
insert into tabel.Counters(CounterId, Counter) values(id, x);
else
update tabel.Counters set Counter = x where CounterId = id;
end if;
-- select the new value and commit the transaction
select x;
commit;
END
The 'for update' statement locks the row in the counters table. This avoids duplicates being made by multiple threads.