July 18, 2008 at 7:00 am
Hi,
I am in no way a db expert, but I am required to create a generic trigger to track changes to a few tables. We don't want to write a specific trigger for each table. We would like to enter the data that has been updated, inserted, deleted into a "generic" table that looks like this:
id (sequence number)
batch_id (identifies a set of related changes; i.e. multiple values were modified in one record with the same action)
table (modified)
column (modified)
original_value
new_value
user_id
timestamp
My problem is to figure out which column has changes in a generic way to just insert the new value and the old value. We ONLY want the column(s) that has been updated,inserted or deleted.
This is what I have so far, but it does not work. Does not seem like I can use computed values in a SELECT statement:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
ALTER TRIGGER [TRG_author]
ON [dbo].[author]
FOR DELETE,INSERT,UPDATE
AS
DECLARE @ins BIT
DECLARE @del BIT
DECLARE @ACT varchar(6)
DECLARE @ColumnCount int
DECLARE @Counter int
DECLARE @TableName varchar(30)
DECLARE @StringColumn varchar (50)
DECLARE @OLD_VALUE varchar(500)
DECLARE @NEW_VALUE varchar(500)
SET @del = 0
SET @ins = 0
IF EXISTS (SELECT TOP 1 1 FROM DELETED)
SET @del=1
IF EXISTS (SELECT TOP 1 1 FROM INSERTED)
SET @ins = 1
IF @ins = 1 AND @del = 1 SET @ACT = 'UPDATE'
IF @ins = 1 AND @del = 0 SET @ACT = 'INSERT'
IF @del = 1 AND @ins = 0 SET @ACT = 'DELETE'
IF @ins = 0 AND @del = 0 RETURN
SELECT @ColumnCount =
(
SELECT count(COLUMN_NAME)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'author'
);
SET @TableName = 'author';
SET @Counter = 0;
WHILE @Counter < @ColumnCount BEGIN
Set @Counter = @Counter + 1
-- find the first column name and subsequent column names
SELECT @StringColumn = (SELECT COL_NAME(OBJECT_ID(@TableName), @Counter) AS 'Column Name')
IF @ACT = 'UPDATE' BEGIN
--UPDATE
PRINT 'SELECT VALUE FROM COLUMN : ' + @StringColumn
SELECT @OLD_VALUE = (SELECT @StringColumn FROM DELETED)
SELECT @NEW_VALUE = (SELECT @StringColumn FROM INSERTED)
PRINT 'OLD_VALUE : ' + @OLD_VALUE
PRINT 'NEW_VALUE : ' + @NEW_VALUE
END
ELSE IF @ACT = 'INSERT'
--INSERT
RETURN
ELSE
--DELETE
RETURN
END
Is there an easier way than looping through the table columns?
I tried
IF UPDATE(columnname) BEGIN
END
But, columnname can't be a computed value...
Any ideas? I have searched and searched for an answer without luck.
Thanks
July 18, 2008 at 7:08 am
hengert (7/18/2008)
Hi,I am in no way a db expert, but I am required to create a generic trigger to track changes to a few tables. We don't want to write a specific trigger for each table. We would like to enter the data that has been updated, inserted, deleted into a "generic" table that looks like this:
May I advise, if you want to go that kind of route, write a procedure to generate triggers for each table based on the system tables.
Looping through the system tables within the trigger is going to increase the time the triggers take to execute, lengthening the transactions and making it more likely that you'll have blocking problems.
So, you would have a proc that generated and executed the CREATE TRIGGER .... sstatement based on the table name and the columns in it.
Gail Shaw
Microsoft Certified Master: SQL Server, MVP, M.Sc (Comp Sci)
SQL In The Wild: Discussions on DB performance with occasional diversions into recoverability
July 18, 2008 at 7:14 am
Sounds like maybe that is the only way to go then.
Now I have to figure out how to write a procedure for that 🙂
Thanks.
July 18, 2008 at 7:19 am
The table we want to have triggers for will probably not be updated very often.
July 18, 2008 at 7:32 am
Lots of dynamic SQL. 😉 If you get stuck, we can help.
One other thing I noticed looking over your trigger code. Be careful of constructs like this in trigger code
SELECT @SomeVariable = SomeColumn FROM inserted
Triggers fire once for a data modification and the inserted and deleted rows will contain all the rows affected by the operation. So if you run an update that affects 25 rows, the update trigger will fire once and both inserted and deleted will contain 25 rows.
The code construct above assumes there is only 1 row in teh inserted table and if there are more, retrieves the value of one of them.
Gail Shaw
Microsoft Certified Master: SQL Server, MVP, M.Sc (Comp Sci)
SQL In The Wild: Discussions on DB performance with occasional diversions into recoverability
July 18, 2008 at 7:45 am
You may be able to use the COLUMNS_UPDATED() function to determine which column(s) have been updated. Check out BOL for details.
As Gail has already mentioned you would be better off from a performance perspective if you wrote a procedure that created a specific audit trigger for each table.
Jack Corbett
Consultant - Straight Path Solutions
Check out these links on how to get faster and more accurate answers:
Forum Etiquette: How to post data/code on a forum to get the best help
Need an Answer? Actually, No ... You Need a Question
July 18, 2008 at 8:07 am
Thanks.
What is LOB? Sorry 🙁
Yeah, I know the code I have does not work for many rows in the updated/deleted table. The code I have I have pieced together from different examples.
How come thi statemetent does not work:
SELECT @StringColumn = (SELECT COL_NAME(OBJECT_ID(@TableName), @Counter) AS 'Column Name')
IF @ACT = 'UPDATE' BEGIN
--UPDATE
PRINT 'SELECT VALUE FROM COLUMN : ' + @StringColumn
SELECT @OLD_VALUE = (SELECT @StringColumn FROM DELETED)
SELECT @NEW_VALUE = (SELECT @StringColumn FROM INSERTED)
PRINT 'OLD_VALUE : ' + @OLD_VALUE
PRINT 'NEW_VALUE : ' + @NEW_VALUE
END
It just returns this:
SELECT VALUE FROM COLUMN : ID
OLD_VALUE : ID
NEW_VALUE : ID
SELECT VALUE FROM COLUMN : firstname
OLD_VALUE : firstname
NEW_VALUE : firstname
SELECT VALUE FROM COLUMN : lastname
OLD_VALUE : lastname
NEW_VALUE : lastname
How would you go about to write a procedure to generate a trigger? I am so new to T-SQL....
Thank you for your help.
July 18, 2008 at 2:36 pm
hengert (7/18/2008)
Thanks.What is LOB? Sorry 🙁
It is BOL not LOB and stands for Books On Line.
How come thi statemetent does not work:
SELECT @StringColumn = (SELECT COL_NAME(OBJECT_ID(@TableName), @Counter) AS 'Column Name')
IF @ACT = 'UPDATE' BEGIN
--UPDATE
PRINT 'SELECT VALUE FROM COLUMN : ' + @StringColumn
SELECT @OLD_VALUE = (SELECT @StringColumn FROM DELETED)
SELECT @NEW_VALUE = (SELECT @StringColumn FROM INSERTED)
PRINT 'OLD_VALUE : ' + @OLD_VALUE
PRINT 'NEW_VALUE : ' + @NEW_VALUE
END
The code above does not work because SQL Server does not recognize a variable as a column name. In order for that to work you would need to use dynamic sql (which I do not think will have access to the Inserted or Deleted tables) like this:
[font="Courier New"]DECLARE @test VARCHAR(100),
@column VARCHAR(50),
@sql NVARCHAR(MAX)
SET @column = 'name'
SET @sql = 'Set @test-2 = (select top 1 ' + @column + ' from sys.objects);'
EXEC sp_executesql @sql, N'@test varchar(100) Output' , @test = @test Output
SELECT @test
[/font]
How would you go about to write a procedure to generate a trigger? I am so new to T-SQL....
You would do something that goes through the Information_Schema views for tables and columns that builds a trigger. Something like:
[font="Courier New"]DECLARE @schema SYSNAME,
@table SYSNAME,
@sql VARCHAR(MAX),
@column SYSNAME
DECLARE c_tables CURSOR FOR
SELECT
table_schema,
table_name
FROM
information_schema.tables
WHERE
table_schema = 'HumanResources' AND
table_type = 'Base table'
OPEN c_tables
FETCH Next FROM c_tables INTO
@schema,
@table
WHILE @@Fetch_Status = 0
BEGIN
DECLARE @primary_key_column SYSNAME
-- assumes you have a single column primary key
--if not you would need to handle differently
-- also assumes each table has a primary key
SELECT
@primary_key_column = CCU.column_name
FROM
INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CCU JOIN
INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU ON
CCU.COLUMN_NAME = KCU.COLUMN_NAME AND
CCU.TABLE_NAME = KCU.TABLE_NAME AND
CCU.TABLE_SCHEMA = KCU.TABLE_SCHEMA AND
CCU.TABLE_CATALOG = KCU.TABLE_CATALOG AND
CCU.CONSTRAINT_NAME = KCU.CONSTRAINT_NAME JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC ON
KCU.TABLE_NAME = TC.TABLE_NAME AND
KCU.TABLE_SCHEMA = TC.TABLE_SCHEMA AND
KCU.TABLE_CATALOG = TC.TABLE_CATALOG AND
KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
WHERE
TC.CONSTRAINT_TYPE = 'PRIMARY KEY' AND
CCU.Table_Schema = @Schema AND
CCU.Table_Name = @Table
SET @sql = 'Create Trigger ' + @schema + '.trg_' + @table + '_ins_upd_del on ' +
@schema + '.' + @table + ' After ' + 'Insert, Update, Delete As If ' +
'Exists(Select * From inserted) and exists(select * from deleted) Begin '
DECLARE c_columns CURSOR FOR
SELECT
column_name
FROM
information_schema.columns
WHERE
table_schema = @schema AND
table_name = @table
OPEN c_columns
FETCH Next FROM c_columns INTO
@column
WHILE @@Fetch_Status = 0
BEGIN
SET @sql = @sql + 'If Update(' + @column + ') Begin Insert into audit ' +
'Select I.' + @column + ' as new_val, D.' + @column +
' as old_val from inserted I Join deleted D On I.' +
@primary_key_column + ' = D.' + @primary_key_column +
' End ' -- need a space
FETCH Next FROM c_columns INTO
@column
END
CLOSE c_columns
DEALLOCATE c_columns
SET @sql = @sql + ' End'
SELECT (@sql)
--Exec (@sql)
FETCH Next FROM c_tables INTO
@schema,
@table
END
CLOSE c_tables
DEALLOCATE c_tables
[/font]
This is not 100% tested nor does it do exactly what you want, but it should set you on the right path.
Jack Corbett
Consultant - Straight Path Solutions
Check out these links on how to get faster and more accurate answers:
Forum Etiquette: How to post data/code on a forum to get the best help
Need an Answer? Actually, No ... You Need a Question
July 21, 2008 at 10:18 am
Thank you for your answer.
This is what I came up with (Keep in mind that this is the first time I write T-SQL)
All I am missing now is how to figure out which user did the delete/insert/update. We have to capture the logged in user and not the database user. I think the issue will be solved if we add the user_id to each table, then the trigger will capture that column change....
CREATE PROC createGenericTrigger (@TableName Varchar(100))
AS
BEGIN
--=== PROC Declarations
DECLARE @ColumnCount int
DECLARE @TriggerName varchar(50)
DECLARE @Counter int
DECLARE @UpdateStatement varchar(MAX)
DECLARE @StringColumn varchar(100)
DECLARE @Declarations varchar(400)
DECLARE @Initializations varchar(400)
-- Fetch the column count for the table to create the trigger for
SELECT @ColumnCount =
(
SELECT count(COLUMN_NAME)
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = @TableName
);
SET @Counter = 0;
Set @UpdateStatement = '';
--== Loop though all the columns in the table to track changes for. For each column
--== create a IF UPDATE(columnname)
WHILE @Counter < @ColumnCount
BEGIN
Set @Counter = @Counter + 1
--== find the first column name and subsequent column names
SELECT @StringColumn = (SELECT COL_NAME(OBJECT_ID(@TableName), @Counter) AS 'Column Name')
--== Create the IF UPDATED statement
Set @UpdateStatement = @UpdateStatement +
--== Make sure we catch which table and column that is updated/deleted/inserted
' SET @COLUMN_NAME = ' + '''' + @StringColumn + '''' +
' SET @TABLE_NAME = ' + '''' + @TableName + '''' +
--== A trigger uses two tables to keep track of changes, DELETED and INSERTED tables
--== If we have an update, then fetch the new and old value
'IF @ACT = ''UPDATE''' +
' BEGIN ' +
' IF UPDATE (' + @StringColumn + ')
BEGIN ' +
' SELECT @OLD_VALUE = (SELECT ' + @StringColumn + ' from deleted) ' +
' SELECT @NEW_VALUE = (SELECT ' + @StringColumn + ' from inserted) ' +
' IF @OLD_VALUE <> @NEW_VALUE ' +
' BEGIN ' +
'EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, @NEW_VALUE' +
' END ' +
'END ' +
'END ' +
--== If we have an insert, then just fetch the new value
'ELSE IF @ACT = ''INSERT''' +
' BEGIN ' +
' SELECT @NEW_VALUE = (SELECT ' + @StringColumn + ' from inserted) ' +
'EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, '''', @NEW_VALUE' +
' END ' +
--== If we have a delete, then just fetch the old value
'ELSE IF @ACT = ''DELETE''' +
' BEGIN ' +
' SELECT @OLD_VALUE = (SELECT ' + @StringColumn + ' from deleted) ' +
'EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, ''''' +
' END '
END
PRINT @UpdateStatement
SET @TriggerName = 'TRG_' + @TableName
--== Declarations for the trigger
SET @Declarations = 'DECLARE @OLD_VALUE varchar(500) ' +
'DECLARE @NEW_VALUE varchar(500) ' +
'DECLARE @TABLE_NAME varchar(500) ' +
'DECLARE @COLUMN_NAME varchar(500) ' +
'DECLARE @ACT CHAR(6) ' +
'DECLARE @del BIT ' +
'DECLARE @ins BIT ' +
'DECLARE @BATCH_ID Binary(16) ' +
'SET @BATCH_ID = NEWID() '
--== Determine what type of query fired the trigger
SET @Initializations ='SET @del = 0 ' +
'SET @ins = 0 ' +
'IF EXISTS (SELECT TOP 1 1 FROM DELETED) SET @del=1 ' +
'IF EXISTS (SELECT TOP 1 1 FROM INSERTED) SET @ins=1 ' +
'IF @ins = 1 AND @del = 1 Set @ACT = ''UPDATE'' ' +
'IF @ins = 1 AND @del = 0 Set @ACT = ''INSERT'' ' +
'IF @ins = 0 AND @del = 1 Set @ACT = ''DELETE'' '
-- == This will execute the sql to create the trigger
EXEC ('CREATE TRIGGER ' + @TriggerName +
' ON ' + @TableName +
' AFTER DELETE, INSERT, UPDATE
AS BEGIN '
+ @Declarations + @Initializations + @UpdateStatement + ' END' )
END
July 21, 2008 at 10:34 am
Two questions:
1. How many tables are you adding triggers to? Dozens? Hundreds? Five?
2. Why a "generic" trigger? Why not a specific trigger per table?
- Gus "GSquared", RSVP, OODA, MAP, NMVP, FAQ, SAT, SQL, DNA, RNA, UOI, IOU, AM, PM, AD, BC, BCE, USA, UN, CF, ROFL, LOL, ETC
Property of The Thread
"Nobody knows the age of the human race, but everyone agrees it's old enough to know better." - Anon
July 21, 2008 at 10:50 am
hengert (7/21/2008)
Thank you for your answer.This is what I came up with (Keep in mind that this is the first time I write T-SQL)
The code looks like it will create a trigger alright. Can you post the code from an auto-generated trigger so we can try to tune it?
All I am missing now is how to figure out which user did the delete/insert/update. We have to capture the logged in user and not the database user. I think the issue will be solved if we add the user_id to each table, then the trigger will capture that column change....
If you are using Windows Authentication or a specific SQL User for each user then SUSER_NAME() which will return domain\user_name for windows and just user_name for SQL Logins.
Jack Corbett
Consultant - Straight Path Solutions
Check out these links on how to get faster and more accurate answers:
Forum Etiquette: How to post data/code on a forum to get the best help
Need an Answer? Actually, No ... You Need a Question
July 21, 2008 at 11:00 am
I have to create a trigger for 9 tables. So, depending on how many columns each table has, it would be a nightmare to write.
July 21, 2008 at 11:03 am
Is there a way to format this better? The generated trigger looks ugly to say the least.
Here is a generated trigger:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
ALTER TRIGGER [TRG_author] ON [dbo].[author] AFTER DELETE, INSERT, UPDATE
AS BEGIN DECLARE @OLD_VALUE varchar(500) DECLARE @NEW_VALUE varchar(500) DECLARE @TABLE_NAME varchar(500) DECLARE @COLUMN_NAME varchar(500) DECLARE @ACT CHAR(6) DECLARE @del BIT DECLARE @ins BIT DECLARE @BATCH_ID Binary(16) SET @BATCH_ID = NEWID() SET @del = 0 SET @ins = 0 IF EXISTS (SELECT TOP 1 1 FROM DELETED) SET @del=1 IF EXISTS (SELECT TOP 1 1 FROM INSERTED) SET @ins=1 IF @ins = 1 AND @del = 1 Set @ACT = 'UPDATE' IF @ins = 1 AND @del = 0 Set @ACT = 'INSERT' IF @ins = 0 AND @del = 1 Set @ACT = 'DELETE' SET @COLUMN_NAME = 'ID' SET @TABLE_NAME = 'author'IF @ACT = 'UPDATE' BEGIN IF UPDATE (ID)
BEGIN SELECT @OLD_VALUE = (SELECT ID from deleted) SELECT @NEW_VALUE = (SELECT ID from inserted) IF @OLD_VALUE <> @NEW_VALUE BEGIN EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, @NEW_VALUE END END END ELSE IF @ACT = 'INSERT' BEGIN SELECT @NEW_VALUE = (SELECT ID from inserted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, '', @NEW_VALUE END ELSE IF @ACT = 'DELETE' BEGIN SELECT @OLD_VALUE = (SELECT ID from deleted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, '' END SET @COLUMN_NAME = 'firstname' SET @TABLE_NAME = 'author'IF @ACT = 'UPDATE' BEGIN IF UPDATE (firstname)
BEGIN SELECT @OLD_VALUE = (SELECT firstname from deleted) SELECT @NEW_VALUE = (SELECT firstname from inserted) IF @OLD_VALUE <> @NEW_VALUE BEGIN EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, @NEW_VALUE END END END ELSE IF @ACT = 'INSERT' BEGIN SELECT @NEW_VALUE = (SELECT firstname from inserted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, '', @NEW_VALUE END ELSE IF @ACT = 'DELETE' BEGIN SELECT @OLD_VALUE = (SELECT firstname from deleted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, '' END SET @COLUMN_NAME = 'lastname' SET @TABLE_NAME = 'author'IF @ACT = 'UPDATE' BEGIN IF UPDATE (lastname)
BEGIN SELECT @OLD_VALUE = (SELECT lastname from deleted) SELECT @NEW_VALUE = (SELECT lastname from inserted) IF @OLD_VALUE <> @NEW_VALUE BEGIN EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, @NEW_VALUE END END END ELSE IF @ACT = 'INSERT' BEGIN SELECT @NEW_VALUE = (SELECT lastname from inserted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, '', @NEW_VALUE END ELSE IF @ACT = 'DELETE' BEGIN SELECT @OLD_VALUE = (SELECT lastname from deleted) EXEC logEvent @BATCH_ID, @ACT, @TABLE_NAME, @COLUMN_NAME, @OLD_VALUE, '' END END
July 21, 2008 at 12:21 pm
hengert (7/21/2008)
I have to create a trigger for 9 tables. So, depending on how many columns each table has, it would be a nightmare to write.
Unless you're talking about hundreds of columns per table (which is unusual), writing nine triggers should take less than half an hour.
create table #Inserted (
ID int primary key,
Col1 varchar(100),
Col2 int)
create table #Deleted (
ID int primary key,
Col1 varchar(100),
Col2 int)
insert into #Inserted (ID, Col1, Col2)
select 1, 'x', 1 union all
select 2, 'y', 2
insert into #Deleted (ID, Col1, Col2)
select 2, 'z', 2 union all
select 3, 'w', 3
insert into dbo.LogTable (LoggedData)
select isnull(i.ID, d.ID) as ID,
i.Col1 as Col1_To, i.Col2 as Col2_To,
d.Col1 as Col1_From, d.Col2 as Col2_From,
case
when i.id is not null and d.id is not null then 'U'
when i.id is null and d.id is not null then 'D'
when i.id is not null and d.id is null then 'I'
end [Action]
from #inserted i
full outer join #deleted d
on i.id = d.id
for xml path
I used temp tables because a script can't directly access the inserted and deleted tables, but this script can be easily modified into quite an efficient logging trigger.
Actually, though, you really don't need to log inserts. If the data is never updated or deleted, the actual row is the "log", and if it is updated or deleted, the "from" columns of the first logged action are the original inserted values.
If you don't want to log colums that aren't changed, use NullIf in the XML query. Like:
NullIf(i.Col1, d.Col1) as Col1_To, NullIf(d.Col1, i.Col1) as Col1_From
Since the XML data type doesn't include Nulls unless you tell it to, it will not log the columns that aren't changed in an update action. (This also means it won't log a null value that's inserted or deleted, but that's easy enough to extract from the log, where it doesn't have that column.)
If your log table has an ID column, that becomes your transaction ID. It includes all rows that were inserted/updated/deleted, in an easy-to-query XML format.
(Using the "Drag the columns" method in Management Studio, and some simple copy-and-paste, I can create a trigger for a 100-column table, using this trigger template, in about 3-5 minutes. A ten-column table takes less than a minute.)
- Gus "GSquared", RSVP, OODA, MAP, NMVP, FAQ, SAT, SQL, DNA, RNA, UOI, IOU, AM, PM, AD, BC, BCE, USA, UN, CF, ROFL, LOL, ETC
Property of The Thread
"Nobody knows the age of the human race, but everyone agrees it's old enough to know better." - Anon
July 21, 2008 at 12:29 pm
Sure you can make it format better by adding Carriage Returns, Line Feeds, and Tabs. You would do this using the CHAR() function. I believe it is CHAR(10) for CR, CHAR(13) for LF, and CHAR(9) for tabs, but you should look it up in BOL.
I just went back over your code that creates the trigger and you should really use the schema as well as the table name when finding the columns as it is possible to have the same table name in 2 schemas. Also if you do use the primary key finding section I provided you can simplify your code for determining the action by joining the inserted and deleted tables.
Jack Corbett
Consultant - Straight Path Solutions
Check out these links on how to get faster and more accurate answers:
Forum Etiquette: How to post data/code on a forum to get the best help
Need an Answer? Actually, No ... You Need a Question
Viewing 15 posts - 1 through 15 (of 21 total)
You must be logged in to reply to this topic. Login to reply