sybase cursors in a trigger - triggers

I am trying to use a cursor in a trigger on a sybase ASE 15.0.3 system running on Solaris. The purpose for this is that I want to know which column of a table is getting updated. This information I then save in an admin table for further lookups.
create trigger test_trigger on my_table for update as
set nocount on
/* declare cursor */
declare #colname varchar(64)
declare column_name cursor for
select syscolumns.name from syscolumns join sysobjects on (sysobjects.id = syscolumns.id) where sysobjects.name = 'my_table'
/* open the cursor */
open column_name
/* fetch the first row */
fetch column_name into #colname
/* now loop, processing all the rows
** ##sqlstatus = 0 means successful fetch
** ##sqlstatus = 1 means error on previous fetch
** ##sqlstatus = 2 means end of result set reached
*/
while (##sqlstatus != 2)
begin
/* check for errors */
if (##sqlstatus = 1)
begin
print "Error in column_names cursor"
return
end
/* now do the insert if colum was updaed */
if update(#colname)
begin
insert into my_save_table (login,tablename,field,action,pstamp)
select suser_name(),'my_table',#colname,'U',getdate() from inserted
end
/* fetch the next row */
fetch column_name into #colname
end
/* close the cursor and return */
close column_name
go
Unfortunately when trying to run this in isql I get the following error:
Msg 102, Level 15, State 1:
Server 'my_sybase_server', Procedure 'test_trigger', Line 34:
Incorrect syntax near '#colname'.
I did some investigations and found out that line 34 means the following statement:
if update(#colname)
then I tried to just check on 1 column and replaced it by
if update(some_column_name)
That actually worked fine and I don't have any other idea how to fix that. It looks like the update() function somehow not allows to contain a variable. I did not find any additional information on the sybase books or anywhere else in google ect. Does anybody may find a solution for this? Is it may a bug? Are there workarounds for the cursor?
Thanks for any advice

The problem is that update(#colname) is something like update('colname') and needs to be update(colname). In order to you achieve that, you need to use Dynamic SQL.
I've already saw the documentation and it's possible to use:
Dynamically executing Transact-SQL
When used with the string or char_variable options, execute concatenates the supplied strings and variables to execute the
resulting Transact-SQL command. This form of the execute command may
be used in SQL batches, procedures, and triggers.
Check this article for an example on how to use dynamic sql!

If it is not a problem to recreate the trigger every time the table is altered (columns added/dropped) you may just generate the body for your trigger with such query
select
'if update('+c.name+')
set #colname = '''+c.name+'''
'
from syscolumns c where id = object_id('my_table')

Related

Why doesn't this function actually update anything?

This is a SQL learning exercise. I have a 'tbl' with a single integer field. There are no indices on this table. I have this function:
CREATE OR REPLACE FUNCTION rcs()
RETURNS VOID AS $$
DECLARE
c CURSOR FOR SELECT * FROM tbl ORDER BY sequence;
s INTEGER;
r RECORD;
BEGIN
s := 0;
OPEN c;
LOOP
FETCH c INTO r;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'got it';
r.sequence = s;
s := s + 1;
RAISE NOTICE '%', r.sequence;
END LOOP;
CLOSE c;
END;
$$ language 'plpgsql'
This loads and runs cleanly and the RAISE statements suggest that the 'sequence' field gets updated to 0, 1 etc. in accordance with the ORDER BY.
However, when I SELECT the table afterwards, the pre-existing values (which happen to all be '6') did not change.
Is this something to do with transactions? I tried fiddling around with COMMIT, etc. to no avail.
This is a freshly installed Postgresql 9.4.4 running on a Linode with no hackery of config files or anything like that, from the 'psql' command line.
EDIT: maybe it's because 'r' isn't actually the DB table, it's some kind of temporary copy of it? If so, please clarify, hopefully what I'm trying to achieve here is obvious (and I know it may be a bit nonsensical, but surely it's possible without resorting to reading the set into Java, etc.)
The actual problem: your function does not contain an UPDATE statement so nothing gets written to disk. r.sequence = s; simply assigns a new value to a variable that is held in memory.
To fix this, you need something like:
UPDATE tbl
set sequence = s -- change the actual column in the table
WHERE current of c; -- for the current row of your cursor
If where current of doesn't work, you need to switch that to a "regular" where clause:
UPDATE tbl
set sequence = s
WHERE tbl.pk_column = r.pk_column; -- the current primary key value
But a much more efficient solution is to do this in a single statement:
update tbl
set sequence = tu.new_sequence
from (
select t.pk_column,
row_number() over (order by t.sequence) as new_sequence
from tbl t
) tu
where tbl.pk_column = tu.pk_column;
You need to replace the column name pk_column with the real primary key (or unique) column of your table.

Define variable and run a query using EXECUTE BLOCK

I have a large query in Firebird (which I run using FlameRobin), using a parameter all over the place, but getting the query below to run will do:
SELECT * FROM customers WHERE customerid = 1234;
I want to define 1234 as a variable, say customerID, so that I can easily replace it with something else.
I've learned that I need to put this inside a EXECUTE BLOCK.
EXECUTE BLOCK
AS
DECLARE customerID INT = 1234;
BEGIN
SELECT * FROM customers WHERE customerid = :customerID
END
If of any importance, the error I am getting is Engine Message :
Dynamic SQL Error
SQL error code = -104
Unexpected end of command - line 3, column 26
The problem is that FlameRobin needs to know when a statement ends and the next statement starts. By default it uses the semicolon (;) for this. However an EXECUTE BLOCK is essentially a stored procedure that isn't stored in the database, so it contains PSQL code that also uses the semicolon as a statement separator.
The consequence of this is that you get syntax errors because FlameRobin is sending incomplete statements to the server (that is: it is sending a statement after each ; it encounters).
You need to instruct FlameRobin to use a different statement terminator using SET TERM. Other Firebird query tools (eg isql) require this as well, but it is not actually part of the syntax of Firebird itself!
So you need to execute your code as:
-- Instruct flamerobin to use # as the terminator
SET TERM #;
EXECUTE BLOCK
AS
DECLARE customerID INT = 1234;
BEGIN
SELECT * FROM customers WHERE customerid = :customerID;
END#
-- Restore terminator to ;
SET TERM ;#
However doing this will still result in an error, because this query is invalid for PSQL: A SELECT in a PSQL block requires an INTO clause to map the columns to variables. And to get the values out of the EXECUTE BLOCK returned to FlameRobin you also need to specify a RETURNS clause as described in the documentation of EXECUTE BLOCK:
-- Instruct flamerobin to use # as the terminator
SET TERM #;
EXECUTE BLOCK
RETURNS (col1 INTEGER, col2 VARCHAR(100))
AS
DECLARE customerID INT = 1234;
BEGIN
SELECT col1, col2 FROM customers WHERE customerid = :customerID INTO :col1, :col2;
SUSPEND;
END#
-- Restore terminator to ;
SET TERM ;#
As far as I know the SUSPEND is technically not required here, but Flamerobin will not fetch the returned row if it isn't included.
However the above will not work if the select produces multiple rows. For that you need to use FOR SELECT ... DO combined with a SUSPEND:
-- Instruct flamerobin to use # as the terminator
SET TERM #;
EXECUTE BLOCK
RETURNS (col1 INTEGER, col2 VARCHAR(100))
AS
DECLARE customerID INT = 1234;
BEGIN
FOR SELECT col1, col2 FROM customers WHERE customerid = :customerID INTO :col1, :col2
DO
SUSPEND;
END#
-- Restore terminator to ;
SET TERM ;#
The SUSPEND here returns the row and waits until the caller has fetched that row and then continues with the FOR loop. This way it will iterate over the results.
IMHO this too much effort for parameterization. You might want to consider simply not parametrizing when you use FlameRobin, or use a tool that supports asking for parameter values for the normal parameter placeholders of Firebird (but to be honest I am not sure if there are any).
Well you need a input and return value, ex:
EXECUTE BLOCK (p_customerID integer=?)
returns(col_1 type of column table.id_cv)
as
begin
for
select col_1 from table
where customerID=:p_customerID
into col_1 do
suspend;
end
Delphi as client side:
myquery1.ParamByName('p_customerID ').Value:=1234;
myquery1.Open()
if not myquery1.IsEmpty then
begin
// using returning values
if myquery1.FieldbyName('col_1').AsString='this' then DoThat();
end;
Use this way ever yout need input parameters and/or returning values;

dynamic sql query in postgres

I was attempting to use Dynamic SQL to run some queries in postgres.
Example:
EXECUTE format('SELECT * from result_%s_table', quote_ident((select id from ids where condition = some_condition)))
I have to query a table, which is of the form result_%s_table wherein, I need to substitute the correct table name (an id) from an another table.
I get the error ERROR: prepared statement "format" does not exist
Link: string substitution with query result postgresql
EXECUTE ... USING only works in PL/PgSQL - ie within functions or DO blocks written in the PL/PgSQL language. It does not work in plain SQL; the EXECUTE in plain SQL is completely different, for executing prepared statements. You cannot use dynamic SQL directly in PostgreSQL's SQL dialect.
Compare:
PL/PgSQL's EXECUTE ... USING; to
SQL's EXECUTE
See the 2nd last par in my prior answer.
In addition to not running except in PL/PgSQL your SQL statement is wrong, it won't do what you expect. If (select id from ids where condition = some_condition) returns say 42, the statement would fail if id is an integer. If it's cast to text you'd get:
EXECUTE format('SELECT * from result_%s_table', quote_ident('42'));
EXECUTE format('SELECT * from result_%s_table', '"42"');
EXECUTE 'SELECT * from result_"42"_table';
That's invalid. You actually want result_42_table or "result_42_table". You'd have to write something more like:
EXECUTE format('SELECT * from %s', quote_ident('result_'||(select id from ids where condition = some_condition)||'_table'))
... if you must use quote_ident.
CREATE OR REPLACE FUNCTION public.exec(
text)
RETURNS SETOF RECORD
LANGUAGE 'plpgsql'
AS $BODY$
BEGIN
RETURN QUERY EXECUTE $1 ;
END
$BODY$;
usage:
select * from exec('select now()') as t(dt timestamptz)
Try using
RETURN QUERY EXECUTE '<SQL Command>'
This will return data into form of table. You have to use this into stored function of PostgreSQL.
I have already created on full demonstration on custom filter and custom sorting using dynamic query of PostgreSQL.
Please visit this url:
http://www.dbrnd.com/2015/05/postgresql-dynamic-sql/
These all look more complicated than the OP's question. A different formatting should do the trick.. but it could absolutely the case that I don't understand.
From how I read OP's question, I think others in a similar situation may benefit from how I got it.
I am using Postgre on Redshift, and I ran into this issue and found a solution.
I was trying to create a dynamic query, putting in my own date.
date = dt.date(2018, 10, 30)
query = ''' select * from table where date >= ''' + str(my_date) + ''' order by date '''
But, the query entirely ignores the condition when typing it this way.
However, if you use the percent sign (%), you can insert the date correctly.
One correct way to write the above statement is:
query = ''' select * from table where date >= ''' + ''' '%s' ''' % my_date + ''' order by date '''
So, maybe this is helpful, or maybe it is not. I hope it helps at least one person in my situation!
Best wishes.
EXECUTE will work only on pl/pqsql environment.
instead of EXECUTE try with SELECT
SELECT format('SELECT * from result_%s_table', quote_ident((select id from ids where condition = some_condition))
output would be the dynamic query.

Updating generator value issue

I'm currently working on modifying a Firebird v. 1.5 database.
The database structure will be modified running queries from a delphi application using interbase components, the problem I'm facing is that I need to run a lot of queries, some of which include creating generators and updating the generator value, the problem is that I need to achieve this in as few queries as possible, but it seems(at least to me) that this is not really possible, what I'm trying to do is the following:
/* this command creates a generator to be used for table TABLENAME */
CREATE GENERATOR GEN_TABLENAME;
So I've created a generator, now I need to set it's value at the current max id from table TABLENAME, like so:
/* one would expect that the following command would work, well it doesn't */
SET GENERATOR GEN_TABLENAME TO (SELECT MAX(ID) FROM TABLENAME);
Now, is there any workaround for this, or am I forced to:
create the generator
get the max id
update the generator value
and repeat process for every table?
I also expected that
SELECT
SELECT MAX(ID) AS ID_TABLENAME_1 FROM TABLENAME_1,
...
SELECT MAX(ID) AS ID_TABLENAME_N FROM TABLENAME_N
would be a workaround to get the max id's from every table in one command, but it doesn't.
Statement
SET GENERATOR GEN_TABLENAME TO (SELECT MAX(ID) FROM TABLENAME);
mixes DDL (SET GENERATOR) and DML (SELECT), AFAIK this is not generally supported and Firebird doesn't support it for sure.
If you can upgrade to the latest version of Firebird then you could use EXECUTE BLOCK and / or EXECUTE STATEMENT to do it all "in one statement" and server side, but with Firebird 1.5 you have to settle for the long way (one statement to get the current max, then another one update the generator).
With the following trick you can set the generator value to the maximum ID value of a table with one SQL statement in Firebird:
SELECT GEN_ID( GEN_TABLENAME,
(SELECT MAX(ID) FROM TABLENAME) - GEN_ID(GEN_TABLENAME, 0)) FROM RDB$DATABASE;
That works, because GEN_ID( <GeneratorName>, <increment>) gets the generator value and increments it by <increment>. This should work in Firebird 1.5 as well as in newer versions.
You could create a stored procedure and call it from Delphi:
create procedure update_generators
as
declare variable max_id integer;
declare variable table_name char(31);
declare variable generator_name char(31);
begin
/* assuming generator naming convention GEN_XXX -> table name XXX */
for select
trim(g.rdb$generator_name),
substring(trim(g.rdb$generator_name) from 5)
from rdb$generators g
where (coalesce(g.rdb$system_flag, 0) = 0)
into
:generator_name,
:table_name
do
begin
/* assuming that the field name is always ID */
execute statement 'select max(id) from ' || :table_name into :max_id;
execute statement 'set generator ' || :generator_name || ' to ' || :max_id;
end
end^
It looks like execute statement is supported by Firebird 1.5 already.
In Firebird 2.0 and later, you could also wrap the code in a execute block and avoid creating a stored procedure.

Meaning of # in a SQL Transaction

Hi I was checking some store procedures of a product installed on my company, you know to see how other people solve problems and learn.
Among this I found this but I do not know what is the meaning of the # in the sql sp in the line select distinct objecttype from #CascadeCollect, any comments please?
This is the whole sp..
begin
-- get all the unique otcs collected in the temp table.
declare #EntityCode int
-- check if the entity requires special casing.
declare #DbCascadeMask int
-- special casing for calendar delete
exec p_DeleteCalendar
declare otccursor cursor for
select distinct objecttype from #CascadeCollect <------ here is the # ....
open otccursor
fetch otccursor into #EntityCode
while ##fetch_status = 0
begin
select #DbCascadeMask = DbCascadeMask
from EntityView as Entity
where Entity.ObjectTypeCode = #EntityCode
if #DbCascadeMask <> 0
begin
exec p_BulkDeleteGeneric #EntityCode
end
fetch otccursor into #EntityCode
end
CLOSE otccursor
DEALLOCATE otccursor
-- Return the count of entity instances that are still not deleted (because they
-- require platform bizlogic/special casing.
select count(*) as NotDeletedCount from #CascadeCollect where processed = 2
end
Thanks for any comments !!!
A single # as a prefix indicates a locally scoped temporary object. In this case it is clearly a table but you can also have #temp procedures as well.
It is only visible to the batch in which it is created (and any child batches) and dropped automatically when the batch exits.
So if that is the whole stored procedure then it is obviously expected to be run from another procedure that actually creates the temp table.
You can also have global temporary objects prefixed with ##.
a table prefixed with # is a local temporary table, it will be dropped once it is out of scope
create table #test(id int)
insert #test values (1)
select * from #test
If you run this from another connection select * from #test the table is not available since it is local