IF statement is not accepted in MySQL trigger, issuing Code 1064 but no reference into the statement - dbvisualizer

I'm using MariaDB (10.3.29-MariaDB-0+deb10u1) and DbVisualizer (Pro 12.1.1 [Build #3237]) as an IDE.
I want to manage the primary key of a table by having MariaDB set the key to a UUID, I started with this
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
SET NEW.pkey = uuid();
which worked perfectly, except that I started providing my own UUIDs, which the trigger over-wrote, of course. I tried the following to create a UUID only if one was not provided:
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
IF NEW.pkey IS NULL THEN
SET NEW.pkey = uuid();
END IF;
The problem is, when I execute the create, I get the following error:
[Code: 1064, SQL State: 42000] (conn=1002) You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version for the right syntax
to use near '' at line 5 [Script position: 132 - 168]
Error 1064 is documented (https://riptutorial.com/mysql/example/2995/error-code-1064--syntax-error) as not correctly using back-ticks, which I can deal with ... except that the message is referring to "syntax to use near '' at line 5", the empty string in the message is less than helpful.
I found found MySQL Trigger with IF statement returning #1064, which adds the use of "delimiter" and tried "|", "//", and "$$" as delimiters. I've tried MANY variations, all similar to the following:
delimiter $$
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
IF NEW.pkey IS null THEN
SET NEW.pkey = uuid();
END IF;
END
$$
delimiter ;
... but none of them worked.
I've looked through DbVisualizer's docs and found nothing that indicates it performs "magic" behind the scenes, but I suspect that if any of the above is correct that DbVis is executing these separately.

Basically You have nor error. Copy the code from the example and use it and you will get any error anymore, you might have some kind of invisible character so delete every thing. and use theis code
CREATE tABLE maintenance (pkey VARCHAR(36))
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
IF NEW.pkey IS null THEN
SET NEW.pkey = uuid();
END IF;
END
INSERT INTO maintenance VALUES (NULL)
✓
SELECT * FROM maintenance
| pkey |
| :----------------------------------- |
| c973e92c-3a6f-11ec-93bc-00163e55bd17 |
db<>fiddle here
here is a Version with DELIMITER
delimiter $$
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
IF NEW.pkey IS null THEN
SET NEW.pkey = uuid();
END IF;
END
$$
delimiter ;

I found this https://mariadb.com/kb/en/trigger-keeps-creating-error/, which led to my solution:
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
SET new.pkey = IF (NEW.pkey is null or NEW.pkey = '', uuid(), NEW.pkey);
Even though this works for this situation, it is not a complete answer because it is not suitable if the trigger needs to execute more complex code.

This is, in fact, a "feature" of dbvisualizer.
The page https://confluence.dbvis.com/display/UG121/Executing+Complex+Statements
describes how to deal with it, the key point is added an '#' to the delimiter statement and the semicolon:
#delimiter $$;
CREATE TRIGGER before_pkey_maintenance
BEFORE INSERT ON maintenance
FOR EACH ROW
IF NEW.pkey IS null THEN
SET NEW.pkey = uuid();
END IF;
$$
#delimiter ;
The DbVis page basically states that it is a level of abstraction above SQL and needs the extended syntax so that it can delimit parts of a more complex script.

Related

Postgres 'after insert or update' trigger isn't firing

I've migrated our database from Oracle to Postgres. One of the steps is to save a copy of the record's status history. I do this via an INSERT, UPDATE trigger on the main table. The trigger checks to see if the status has changed and, if so, adds a record to the status history table.
CREATE OR REPLACE FUNCTION SAMPLE_STATUS_HISTORY_Trigger()
RETURNS TRIGGER AS $$
BEGIN
IF( old.STATUS_CODE != new.STATUS_CODE ) THEN
INSERT INTO SAMPLE_STATUS_HISTORY( analyte_status_history_id, sample_result_id, STATUS_CODE, status_date, status_user_id )
VALUES( nextval('SAMPLE_ANALYTE_STATUS_HIST_SEQ'), new.sample_result_id, new.STATUS_CODE, new.status_date, new.status_user_id );
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER TRG_SAMPLE_ANALYTE_STATUS_HIST
AFTER INSERT OR UPDATE ON SAMPLE_RESULTS
FOR EACH ROW
EXECUTE PROCEDURE SAMPLE_STATUS_HISTORY_Trigger()
;
I've ensured the trigger is bound to the table.
The main application is a .NET MVC app using NHibernate. Things work within a transaction, but the transactions are working for the main table's insert/updates.
My only guess is that I have a syntax error in the trigger logic but I'm not seeing the error if I manually run an insert or update statement.
For insert, OLD.? is null so the inequality check will also return null.
Instead, use
IF( old.STATUS_CODE IS DISTINCT FROM new.STATUS_CODE ) THEN
The INSERT with having a null value for old. was causing me issues when I tried #JGH's suggestion. I ended up with this code which just feels dirty:
IF( TG_OP = 'INSERT' OR old.STATUS_CODE <> new.STATUS_CODE ) THEN
I still don't understand why the original code wouldn't work for UPDATE statements. The only difference between this and that is the !=. Maybe that's a syntax issue for plpgsql?
Either way, this appears to work from both a SQL window and the application.

Trigger | how to delete row instead of update based on cell value

Postgresql 10/11.
I need to delete row instead of update in case if target cell value is null.
So I created this trigger function:
CREATE OR REPLACE FUNCTION delete_on_update_related_table() RETURNS trigger
AS $$
DECLARE
refColumnName text = TG_ARGV[0];
BEGIN
IF TG_NARGS <> 1 THEN
RAISE EXCEPTION 'Trigger function expects 1 parameters, but got %', TG_NARGS;
END IF;
EXECUTE 'DELETE FROM ' || TG_TABLE_NAME || ' WHERE $1 = ''$2'''
USING refColumnName, OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
And a BEFORE UPDATE trigger:
CREATE TRIGGER proper_delete
BEFORE UPDATE OF def_id
ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL)
EXECUTE PROCEDURE delete_on_update_related_table('def_id');
Table is simple:
id uuid primary key
def_id uuid not null
Test:
UPDATE definition_products SET
def_id = NULL
WHERE id = 'f47415e8-6b00-4c65-aeb8-cadc15ca5890';
-- rows affected 0
Documentation says:
Row-level triggers fired BEFORE can return null to signal the trigger
manager to skip the rest of the operation for this row (i.e.,
subsequent triggers are not fired, and the INSERT/UPDATE/DELETE does
not occur for this row).
Previously, I used a RULE instead of the trigger. But there is no way to use WHERE & RETURNING clause in same rule.
You need an unconditional ON UPDATE DO INSTEAD rule with a RETURNING clause
So, is there a way?
While Jeremy's answer is good, there is still room for improvement.
Problems
You need to be very accurate in the definition of the objective. Your statement:
I need to delete row instead of update in case if target cell value is null.
... does not imply that the column was changed to NULL in the UPDATE at hand. Might have been NULL before, like, before you implemented the trigger. So not:
BEFORE UPDATE OF def_id ON public.definition_products
But just:
BEFORE UPDATE ON public.definition_products
Of course, if the column is defined NOT NULL (as it probably should be), there is no effective difference - except for the noise and an additional point of failure. The manual:
A column-specific trigger (one defined using the UPDATE OFcolumn_name syntax) will fire when any of its columns are listed as targets in the UPDATE command's SET list. It is possible for a column's value to change even when the trigger is not fired, because changes made to the row's contents by BEFORE UPDATE triggers are not considered.
Also, nothing in your question indicates the need for dynamic SQL. (That would be the case if you wanted to reuse the same trigger function for multiple triggers on different tables. And even then it's often better to just create several distinct trigger functions for multiple reason: simpler, faster, less error-prone, easier to read & maintain, ...)
As for "error-prone": your original dynamic statement was just invalid:
EXECUTE 'DELETE FROM ' || TG_TABLE_NAME || ' WHERE $1 = ''$2'''
USING refColumnName, OLD.id;
Can't pass a column name as value (refColumnName).
Can't put single quotes around $2, which is passed as value and hence needs no quoting.
An unqualified, unquoted TG_TABLE_NAME can go terribly wrong, which is especially critical for a heavy-weight function that deletes rows.
Jeremy's version fixes most, but still features the unqualified TG_TABLE_NAME.
This would be good:
EXECUTE format('DELETE FROM %s WHERE %I = $1', TG_RELID::regclass, refColumnName) -- refColumnName still unquoted
USING OLD.id;
Or:
EXECUTE format('DELETE FROM %I.%I WHERE %I = $1', TG_TABLE_SCHEMA, TG_TABLE_NAME, refColumnName)
USING OLD.id;
Related:
Why does a PostgreSQL SELECT query return different results when a schema name is specified?
Table name as a PostgreSQL function parameter
Solution
Simpler trigger function:
CREATE OR REPLACE FUNCTION delete_on_update_related_table()
RETURNS trigger AS
$func$
BEGIN
DELETE FROM public.definition_products WHERE id = OLD.id; -- def_id?
RETURN NULL;
END
$func$ LANGUAGE plpgsql;
Simpler trigger:
CREATE TRIGGER proper_delete
BEFORE UPDATE ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL) -- that's the defining condition!
EXECUTE PROCEDURE delete_on_update_related_table(); -- no parameter
You probably want to use OLD.id, not OLD.def_id. (The row to delete is best defined by it's PK, not by the column changed to NULL.) But that's not entirely clear.
This works for me, with a few small changes:
CREATE OR REPLACE FUNCTION delete_on_update_related_table() RETURNS trigger
AS $$
DECLARE
refColumnName text = quote_ident(TG_ARGV[0]);
BEGIN
IF TG_NARGS <> 1 THEN RAISE EXCEPTION 'Trigger function expects 1 parameters, but got %', TG_NARGS; END IF;
EXECUTE format('DELETE FROM %s WHERE %s = %s', quote_ident(TG_TABLE_NAME), refColumnName, quote_literal(OLD.id));
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- create trigger
CREATE TRIGGER proper_delete
BEFORE UPDATE OF def_id
ON public.definition_products
FOR EACH ROW
WHEN (NEW.def_id IS NULL)
EXECUTE PROCEDURE delete_on_update_related_table('id'); --Note id, not def_id

How to return values from dynamically generated "insert" command?

I have a stored procedure that performs inserts and updates in the tables. The need to create it was to try to centralize all the scan functions before inserting or updating records. Today the need arose to return the value of the field ID of the table so that my application can locate the registry and perform other stored procedures.
Stored procedure
SET TERM ^ ;
CREATE OR ALTER procedure sp_insupd (
iaction varchar(3),
iusuario varchar(20),
iip varchar(15),
imodulo varchar(30),
ifieldsvalues varchar(2000),
iwhere varchar(1000),
idesclogs varchar(200))
returns (
oid integer)
as
declare variable vdesc varchar(10000);
begin
if (iaction = 'ins') then
begin
vdesc = idesclogs;
/*** the error is on the line below ***/
execute statement 'insert into '||:imodulo||' '||:ifieldsvalues||' returning ID into '||:oid||';';
end else
if (iaction = 'upd') then
begin
execute statement 'select '||:idesclogs||' from '||:imodulo||' where '||:iwhere into :vdesc;
execute statement 'execute procedure SP_CREATE_AUDIT('''||:imodulo||''');';
execute statement 'update '||:imodulo||' set '||:ifieldsvalues||' where '||:iwhere||';';
end
insert into LOGS(USUARIO, IP, MODULO, TIPO, DESCRICAO) values (
:iusuario, :iip, :imodulo, (case :iaction when 'ins' then 1 when 'upd' then 2 end), :vdesc);
end^
SET TERM ; ^
The error in the above line is occurring due to syntax error. The procedure is compiled normally, that is, the error does not happen in the compilation, since the line in question is executed through the "execute statement". When there was no need to return the value of the ID field, the procedure worked normally with the line like this:
...
execute statement 'insert into '||:imodulo||' '||:ifieldsvalues||';';
...
What would be the correct way for the value of the ID field to be stored in the OID variable?
What is REAL VALUE in ifieldsvalues ?
you can not have BOTH
'insert into '||:imodulo||' '||:ifieldsvalues
'update '||:imodulo||' set '||:ifieldsvalues
because methods to specify column names and column values in INSERT and UPDATE statements is fundamentally different!!! You either would have broken update-stmt or broken insert-stmt!
The error in the above line is occurring due to syntax error
This is not enough. Show the real error text, all of it.
It includes the actual command you generate and it seems you had generated it really wrong way.
all the scan functions before inserting or updating records
Move those functions out of the SQL server and into your application server.
Then you would not have to make insert/update in that "strings splicing" way, which is VERY fragile and "SQL injection" friendly. You stepped into the road to hell here.
the error does not happen in the compilation
Exactly. And that is only for starters. You are removing all the safety checks that should had helped you in applications development.
http://searchsoftwarequality.techtarget.com/definition/3-tier-application
https://en.wikipedia.org/wiki/Multitier_architecture#Three-tier_architecture
http://bobby-tables.com
On modern Firebird versions EXECUTE STATEMENT command can have the same INTO clause as PSQL SELECT command.
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql-coding.html#fblangref25-psql-execstmt
Use http://translate.ru to read http://www.firebirdsql.su/doku.php?id=execute_statement
Or just see SQL examples there. Notice, however, those examples all use SELECT dynamic command, not INSERT. So I am not sure it would work that way.
This works in Firebird 2.5 (but not in Firebird 2.1) PSQL blocks.
execute statement 'insert into Z(payload) values(2) returning id' into :i;
To run it from IBExpert/FlameRobin/iSQL interactive shell add that obvious boilerplate:
execute block returns (i integer) as
begin
execute statement 'insert into Z(payload) values(2) returning id' into :i;
suspend;
end

PostgreSQL trigger raises error 55000

after migrating from PostgreSQL server version 9 to 8.4 I have encountered very strange error.
Short description:
If there is a trigger on a given table for each row before insert or update and one uses in conditional statement (if-else) TG_OP value check and OLD object, following error raises when doinng INSERT:
ERROR: record "old" is not assigned yet
DETAIL: The tuple structure of a not-yet-assigned record is indeterminate.
Detailed description:
There is following DB structure:
CREATE TABLE table1
(
id serial NOT NULL,
name character varying(256),
CONSTRAINT table1_pkey PRIMARY KEY (id)
)
WITH (OIDS=FALSE);
CREATE OR REPLACE FUNCTION exemplary_function()
RETURNS trigger AS
$BODY$ BEGIN
IF TG_OP = 'INSERT' OR OLD.name <> NEW.name THEN
NEW.name = 'someName';
END IF;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
CREATE TRIGGER trigger1
BEFORE INSERT OR UPDATE
ON table1
FOR EACH ROW EXECUTE PROCEDURE exemplary_function();
and following SQL query that triggers error:
INSERT INTO table1 (name) VALUES ('other name')
It seems like parser is not stopping on TG_OP = 'INSERT' condition (and it should, because it is true) but checks another one and that triggers an error.
What's interesting, I was only able to reproduce it on version 8.4.
Postgres doesn't officially do short cuts on boolean statements (Unlike C for example)
It does say it that sometimes it can decide to short cut (see docs) but it might just easily decide to short cut on the second expression rather than the first.
It basically looks at how complicated the expressions on each side are before deciding the evaluation order. Then if that is TRUE it can decide not to bother with the other side.
In this case, it looks like its trying to interpret OLD while its still trying to decide the best order in which to evaluate the expression.
You should be able get around this by using a CASE to split the expressions eg.
IF (CASE WHEN TG_OP = 'INSERT' THEN TRUE ELSE OLD.name <> NEW.name END) THEN

ERROR: unterminated quoted string at or near

While executing below shown trigger code using ANT I am getting the error
org.postgresql.util.PSQLException: ERROR: unterminated quoted string at or near "' DECLARE timeout integer"
Position: 57
I am able to sucessfully execute the below code through PGADmin (Provided by postgres) and command line utility "psql" and the trigger function is added but while executing through ANT it fails everytime
BEGIN TRANSACTION;
CREATE OR REPLACE FUNCTION sweeper() RETURNS trigger as '
DECLARE
timeout integer;
BEGIN
timeout = 30 * 24 * 60 * 60 ;
DELETE FROM diagnosticdata WHERE current_timestamp - teststarttime > (timeout * ''1 sec''::interval);
return NEW;
END;
' LANGUAGE 'plpgsql';
-- Trigger: sweep on diagnosticdata
CREATE TRIGGER sweep
AFTER INSERT
ON diagnosticdata
FOR EACH ROW
EXECUTE PROCEDURE sweeper();
END;
I encountered this error in liquibase and this page was one of the first search results so I guess I share my solution at this page:
You can put your whole sql in a separate file and include this in the changeset.
Its important to set the splitStatements option to false.
The whole changeset would then look like
<changeSet author="fgrosse" id="530b61fec3ac9">
<sqlFile path="your_sql_file_here.sql" splitStatements="false"/>
</changeSet>
I always like to have those big SQL parts (like function updates and such) in separate files.
This way you get proper syntax highlighting when opening the sql file and dont have to intermix XML and SQL in one file.
Edit: as mentioned in the comments its worth noting that the sql change supports the splitStatements option as well (thx to AndreyT for pointing that out).
I had the same problem with the JDBC driver used by Liquibase.
It seems that the driver explodes each line ended by a semicolon and runs it as a separate SQL command. That is why the code below will be executed by the JDBC driver in the following sequence:
CREATE OR REPLACE FUNCTION test(text) RETURNS VOID AS ' DECLARE tmp text
BEGIN tmp := "test"
END;
' LANGUAGE plpgsql
Of course, this is invalid SQL and causes the following error:
unterminated dollar-quoted string at or near ' DECLARE tmp text
To correct this, you need to use backslashes after each line ended with semicolon:
CREATE OR REPLACE FUNCTION test(text)
RETURNS void AS ' DECLARE tmp text; \
BEGIN
tmp := "test"; \
END;' LANGUAGE plpgsql;
Alternatively, you can place the whole definition in one line.
I am using HeidiSQL client and this was solved by placing DELIMITER // before CREATE OR REPLACE statement. There is a also a 'Send batch in one go' option in HeidiSQL that essentially achieves the same thing.
This error arises as an interaction between the particular client used to connect to the server and the form of the function. To illustrate:
The following code will run without casualty in Netbeans 7, Squirrel, DbSchema, PgAdmin3
CREATE OR REPLACE FUNCTION author.revision_number()
RETURNS trigger AS
$BODY$
begin
new.rev := new.rev + 1;
new.revised := current_timestamp;
return new;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
Please note that the 'begin' statement comes immediately after the '$' quoted string.
The next code will halt all the above clients except PgAdmin3.
CREATE OR REPLACE FUNCTION author.word_count()
RETURNS trigger AS
$BODY$
declare
wordcount integer := 0; -- counter for words
indexer integer := 1; -- position in the whole string
charac char(1); -- the first character of the word
prevcharac char(1);
begin
while indexer <= length(new.blab) loop
charac := substring(new.blab,indexer,1); -- first character of string
if indexer = 1 then
prevcharac := ' '; -- absolute start of counting
else
prevcharac := substring(new.blab, indexer - 1, 1); -- indexer has increased
end if;
if prevcharac = ' ' and charac != ' ' then
wordcount := wordcount + 1;
end if;
indexer := indexer + 1;
end loop;
new.words := wordcount;
return new;
end;
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;
The crucial difference in the second example is the 'declare' section. The ploy of using back-slashes raises an error with PgAdmin3.
In summary I suggest trying different tools. Some tools even though they are supposed to be writing text files put invisible stuff into the text. Notoriously this occurs with the Unicode BOM which will halt any php file that tries to implement sessions or namespaces.
Whilst this is no solution I hope it helps.
I had the same problem with zeos and c++ builder.
The solution in my case:
Change the property delimiter (usually ";") to another in the component (class) I used.
dm->ZSQLProcessor1->DelimiterType=sdGo;
Perhaps Ant have something similar.
I know this question was asked a long time ago but I had kind of the same issue with a Postgresql script (run from Jenkins) using Ant's SQL Task.
I tried to run this SQL (saved in a file named audit.sql):
DROP SCHEMA IF EXISTS audit CASCADE
;
CREATE SCHEMA IF NOT EXISTS audit AUTHORIZATION faktum
;
CREATE FUNCTION audit.extract_interval_trigger ()
RETURNS trigger AS $extractintervaltrigger$
BEGIN
NEW."last_change_ts" := current_timestamp;
NEW."last_change_by" := current_user;
RETURN NEW;
END;
$extractintervaltrigger$ LANGUAGE plpgsql
;
but got the error "unterminated dollar-quoted string". No problem running it from pgAdmin.
I found out that it is not the driver that split the script at every ";" but rather Ant.
At http://grokbase.com/t/postgresql/pgsql-jdbc/06cjx3s3y0/ant-sql-tag-for-dollar-quoting I found the answer:
Ant eats double-$$ as part of its variable processing. You have to use
$BODY$ (or similar) in the stored procs, and put the delimiter on its
own line (with delimitertype="row"). Ant will cooperate then.
My Ant SQL script looks like this and it works:
<sql
driver="org.postgresql.Driver" url="jdbc:postgresql://localhost:5432/jenkins"
userid="user" password="*****"
keepformat="true"
autocommit="true"
delimitertype="row"
encoding="utf-8"
src="audit.sql"
/>
This example worked for me with PostgreSQL 14.1 and HeidiSQL 9.4.0.5125
DROP TABLE IF EXISTS emp;
CREATE TABLE emp (
empname text NOT NULL,
salary integer
);
DROP TABLE IF EXISTS EMP_AUDIT;
CREATE TABLE emp_audit(
operation char(1) NOT NULL,
stamp timestamp NOT NULL,
userid text NOT NULL,
empname text NOT NULL,
salary integer
);
DELIMITER //
CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $$
BEGIN
--
-- Create a row in emp_audit to reflect the operation performed on emp,
-- make use of the special variable TG_OP to work out the operation.
--
IF (TG_OP = 'DELETE') THEN
INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
RETURN OLD;
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
RETURN NEW;
ELSIF (TG_OP = 'INSERT') THEN
INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
RETURN NEW;
END IF;
RETURN NULL; -- result is ignored since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS emp_audit ON emp;
CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
FOR EACH ROW EXECUTE PROCEDURE process_emp_audit();
I was receiving the same error because I had my semicolon in a new line like this:
WHERE colA is NULL
;
Make sure they are in a single line as
WHERE colA is NULL;