in the code below when I run it in Degug mode I can see the variables contain values, however when I select them they show NULL, any ideas? I need to eventually do an Update back to the table [dbo].[HistData]
with the values where RecordID = some number. Any ideas welcome.
-- Declare the variables to store the values returned by FETCH.
DECLARE #HD_TckrPercent decimal(6,3) -- H2 in above formula
DECLARE #HD_CloseLater decimal(9,2) -- F2 in above formula
DECLARE #HD_CloseEarlier decimal(9,2) -- F3 in above formula
DECLARE #RowsNeeded INT
DECLARE #RecordCOUNT INT
SET #RowsNeeded = 2
set #RecordCOUNT = 0 -- to initialize it
DECLARE stocks_cursor CURSOR FOR
SELECT top (#RowsNeeded) [TCKR%], [Stock_Close] FROM [dbo].[HistData]
ORDER BY [RecordID]
OPEN stocks_cursor
-- Perform the first fetch and store the values in variables.
-- Note: The variables are in the same order as the columns
-- in the SELECT statement.
-- Check ##FETCH_STATUS to see if there are any more rows to fetch.
WHILE ##FETCH_STATUS = 0
BEGIN
-- Concatenate and display the current values in the variables.
-- This is executed as long as the previous fetch succeeds.
set #RecordCOUNT = (#RecordCOUNT + 1)
Print #HD_CloseLater
IF #RecordCOUNT = 1
BEGIN
FETCH NEXT FROM stocks_cursor
INTO #HD_TckrPercent, #HD_CloseLater
END
ELSE
BEGIN
FETCH NEXT FROM stocks_cursor
INTO #HD_TckrPercent, #HD_CloseEarlier
END
Select #HD_TckrPercent
Select #HD_CloseLater
Select #HD_CloseEarlier
END
CLOSE stocks_cursor
DEALLOCATE stocks_cursor
GO
Related
The goal of the below script is to delete all records in a table for all the distinct users on it except the two first records for each user.
The thing is that the script goes into an infinite loop between these two lines
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
The complete script is
DECLARE #Event int, #User int;
DECLARE cUsers CURSOR STATIC LOCAL FOR SELECT DISTINCT(UserID) FROM Identifications;
OPEN cUsers
FETCH NEXT FROM cUsers INTO #User;
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
BEGIN
DECLARE cRows CURSOR STATIC LOCAL FOR
SELECT EventIdentificacionId FROM Identifications WHERE UserId = #User AND EventIdentificacionId NOT IN
(SELECT TOP 2 EventIdentificacionId FROM Identifications WHERE UserId = #User ORDER BY EventIdentificacionId);
OPEN cRows
FETCH NEXT FROM cRows INTO #Event;
WHILE ##FETCH_STATUS = 0
BEGIN
DELETE FROM Identifications WHERE EventIdentificacionId = #Event;
FETCH NEXT FROM cRows INTO #Event;
END
CLOSE cRows;
DEALLOCATE cRows;
FETCH NEXT FROM cUsers INTO #User;
END
CLOSE cUsers;
DEALLOCATE cUsers;
Can anybody give me some solution/explanation please?
As I wrote in my comment, There are far better ways to do such a thing than using a cursor, let alone a couple of nested cursors.
One such better option is to use a common table expression and row_number, and then delete the rows directly from the common table expression.
I'm not entirely sure this code is correct because I have no real way to test it as you didn't provide sample data or desired results, but I came up with that based on the code in the question:
;WITH CTE AS
(
SELECT UserId,
EventIdentificacionId,
ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY EventIdentificacionId) As Rn
FROM Identifications
)
DELETE
FROM CTE
WHERE Rn > 2 -- Delete all but the first two rows
Change this line as shown:
DECLARE #Event int = 0, #User int = 0;
And remove this line
SET #Event = 0;
The reason you have an infinite loop is that this code:
WHILE ##FETCH_STATUS = 0
SET #Event = 0;
BEGIN
Is actually this:
-- A loop of a single instruction, with no exit criteria
WHILE ##FETCH_STATUS = 0 SET #Event = 0;
-- begin a new code block, with no condition or loop
BEGIN
I have written a stored procedure for inserting name and email address:
CREATE PROCEDURE [dbo].[usp_Referral_Email]
#user_key varchar(36),
#name nvarchar(100),
#email nvarchar(500),
#result int output
AS
BEGIN
DECLARE #username Nvarchar(500)
DECLARE #useremail Nvarchar(500)
DECLARE #CusrsorID CURSOR
SET #CusrsorID = CURSOR FOR
SELECT Value,Value1
FROM ufn_split_string(#name,#email)
OPEN #CusrsorID
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
WHILE ##FETCH_STATUS = 0
BEGIN
declare #user nvarchar(36)
begin try
begin transaction trans_Referral_Email
IF NOT EXISTS (SELECT 1 FROM dbo.C_User_Credentials
WHERE email = #useremail)
BEGIN
IF NOT EXISTS (SELECT 1 FROM dbo.Referral_Email
WHERE R_Email = #useremail)
BEGIN
INSERT INTO dbo.Referral_Email (CFK_C_UP_key, R_Name, R_Email)
VALUES (#user_key, #username, #useremail)
SET #result = 1
END
ELSE
BEGIN
SET #result = 0
END
END
ELSE
BEGIN
SET #result = 0
END
COMMIT transaction trans_Referral_Email
end try
begin catch
rollback transaction trans_Referral_Email
set #result=ERROR_MESSAGE()
end catch
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
END
CLOSE #CusrsorID
DEALLOCATE #CusrsorID
END
As a example I will pass value
#name = ramesh,suresh,rahul
#email = ramesh#gmail.com,suresh#gmail.com,rahul#gmail.com
before inserting we checking condition email address are exists or not, suppose email address is exist it will not insert into the table.
Now my problem is i will explain through example. ramesh#gmail.com and suresh#gmail.com are new email address both email address are will insert into the table so return value is 1 rahul#gmail.com already exist in table so it will insert into the table so return value will be 0 and output #return value will be 0 but we have inserted 2 email address so i need #return value should be 1 as out put.
So my question is if at any place of email address is insert into the table if one email address also insert output should be #return=1
What you need is known as a "latch" (archaic) or as a flag variable, and is pretty common.
A flag variable (in this case, #result) should be initialized outside the loop and then set when a condition arises (in this case, a record is inserted). The variable should not be touched when any subsequent records are skipped. That way it acts as a sort of an OR gate.
Like this:
CREATE PROCEDURE [dbo].[usp_Referral_Email]
#user_key varchar(36),
#name nvarchar(100),
#email nvarchar(500),
#result int output
AS
BEGIN
DECLARE #username Nvarchar(500)
DECLARE #useremail Nvarchar(500)
DECLARE #CusrsorID CURSOR
SET #CusrsorID = CURSOR FOR
SELECT Value,Value1
FROM ufn_split_string(#name,#email)
OPEN #CusrsorID
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
SET #result = 0 --<--- Will stay 0 until one or more rows are inserted
WHILE ##FETCH_STATUS = 0
BEGIN
declare #user nvarchar(36)
begin try
begin transaction trans_Referral_Email
IF NOT EXISTS (SELECT 1 FROM dbo.C_User_Credentials
WHERE email = #useremail)
BEGIN
IF NOT EXISTS (SELECT 1 FROM dbo.Referral_Email
WHERE R_Email = #useremail)
BEGIN
INSERT INTO dbo.Referral_Email (CFK_C_UP_key, R_Name, R_Email)
VALUES (#user_key, #username, #useremail)
SET #result = 1 --<--- Will stay 1 for the rest of its lifespan, even if other rows are not inserted
END
END
COMMIT transaction trans_Referral_Email
end try
begin catch
rollback transaction trans_Referral_Email
set #result=ERROR_MESSAGE()
end catch
FETCH NEXT FROM #CusrsorID INTO #username, #useremail
END
CLOSE #CusrsorID
DEALLOCATE #CusrsorID
END
Notice I've removed a bunch of the ELSE conditions, since you don't need to do anything when a record is skipped.
you are trying to process/insert several users with 1 stored procedure call and you can't use a single output INT field to return back insert status for several users.
Better to split #name and #email parameters at application level and pass to your (modified) stored procedure only a SINGLE pair of name and email. You will then have to call the spT from application level several times for each name/email pair.
If you still want to use a single spT for batch user insert, you will have to record each insert status into a temp table or table variable and then at the spT end you will have to SELECT from that temp table or table variable.
This way at application level you will have a row with status returned for each name/email input pair.
But I personally suggest you actually change your spT to be called once per each name/email pair. It's a better and cleaner approach.
HTH
I wanted to create a new set of tables in a db using a stored procedure and a build table.
The build table includes the following columns and some sample rows:
tblNm colNm colTyp colLen colReq colWarning colUni colComUni
account personID Decimal NULL 0 0 0 0
account studentNum String 15 0 0 0 0
I was considering using multiple cursors as a form of nested looping, but I cannot figure out how to define the column parameters in the nested procedure because cursors only return one value.
I am considering to build an alter statement that parses these values. How could I do this?
You can solve the problem by using two cursors or one cursor. Two cursors will make the code more readable. One cursor will be more efficient.
Two cursors
The code below demonstrates how to use two cursors to iterate through the tables and columns.
DECLARE #tblNm VARCHAR(MAX)
DECLARE cTables CURSOR FOR
SELECT tblNm
FROM CompositeSchema
GROUP BY tblNm
ORDER BY tblNm
OPEN cTables
FETCH cTables INTO #tblNm
WHILE ##FETCH_STATUS=0
BEGIN
PRINT 'Processing table '+#tblNm
-- Start of code to execute for each table
DECLARE #sqlCreateTable VARCHAR(MAX)
SET #sqlCreateTable = 'CREATE TABLE ['+#tblNm+'] ('
DECLARE #colNm VARCHAR(MAX),#colTyp VARCHAR(MAX),#colLen INT,#colReq BIT,#colWarning BIT,#colUni BIT,#colComUni BIT
DECLARE #isFirst BIT
SET #isFirst = 1
DECLARE cCols CURSOR FOR
SELECT colNm,colTyp,colLen,colReq,colWarning,colUni,colComUni
FROM CompositeSchema
WHERE tblNm=#tblNm
ORDER BY colComUni DESC,colNm ASC
OPEN cCols
FETCH cCols INTO #colNm,#colTyp,#colLen,#colReq,#colWarning,#colUni,#colComUni
WHILE ##FETCH_STATUS=0
BEGIN
PRINT 'Processing column ['+#tblNm+'].['+#colNm+']'
-- Start of code to process each column (simplified!)
IF #isFirst=0
SET #sqlCreateTable = #sqlCreateTable+','
SET #isFirst = 0
SET #sqlCreateTable = #sqlCreateTable+'['+#colNm+'] '+#colTyp
IF NOT #colLen IS NULL
SET #sqlCreateTable = #sqlCreateTable+'('+CAST(#colLen AS VARCHAR)+')'
-- End of code to process each column
FETCH cCols INTO #colNm,#colTyp,#colLen,#colReq,#colWarning,#colUni,#colComUni
END
CLOSE cCols
DEALLOCATE cCols
SET #sqlCreateTable = #sqlCreateTable+')'
PRINT #sqlCreateTable
-- EXEC(#sqlCreateTable)
-- End of code to execute for each table
FETCH cTables INTO #tblNm
END
CLOSE cTables
DEALLOCATE cTables
One cursor
In this case we use just one cursor. We keep track of what the current table is that we are processing in the #currentTblNm variable. Whenever the variable changes, we create all columns at once.
DECLARE #currentTblNm VARCHAR(MAX),#sqlCreateTable VARCHAR(MAX)
SET #currentTblNm = ''
DECLARE #tblNm VARCHAR(MAX),#colNm VARCHAR(MAX),#colTyp VARCHAR(MAX),#colLen INT,#colReq BIT,#colWarning BIT,#colUni BIT,#colComUni BIT
DECLARE #isFirst BIT
SET #isFirst = 1
DECLARE cCols CURSOR FOR
SELECT tblNm,colNm,colTyp,colLen,colReq,colWarning,colUni,colComUni
FROM CompositeSchema
ORDER BY tblNm ASC,colComUni DESC,colNm ASC
OPEN cCols
FETCH cCols INTO #tblNm,#colNm,#colTyp,#colLen,#colReq,#colWarning,#colUni,#colComUni
WHILE ##FETCH_STATUS=0
BEGIN
IF #currentTblNm<>#tblNm
BEGIN
IF #sqlCreateTable<>''
BEGIN
SET #sqlCreateTable = #sqlCreateTable+')'
PRINT #sqlCreateTable
--EXEC (#sqlCreateTable)
END
SET #isFirst = 1
SET #sqlCreateTable = 'CREATE TABLE ['+#tblNm+'] ('
SET #currentTblNm = #tblNm
PRINT 'Processing table ['+#tblNm+']'
END
-- Start of code to process each column (simplified!)
IF #isFirst=0
SET #sqlCreateTable = #sqlCreateTable+','
SET #isFirst = 0
SET #sqlCreateTable = #sqlCreateTable+'['+#colNm+'] '+#colTyp
IF NOT #colLen IS NULL
SET #sqlCreateTable = #sqlCreateTable+'('+CAST(#colLen AS VARCHAR)+')'
-- End of code to process each column
FETCH cCols INTO #tblNm,#colNm,#colTyp,#colLen,#colReq,#colWarning,#colUni,#colComUni
END
CLOSE cCols
DEALLOCATE cCols
IF #sqlCreateTable<>''
BEGIN
SET #sqlCreateTable = #sqlCreateTable+')'
PRINT #sqlCreateTable
-- EXEC(#sqlCreateTable)
END
Both pieces of code, Two cursors and One cursor are simplified. The logic to properly create all of the constraints (like primary key, unique constraints, foreign keys etc.), the logic to properly map the column data types, and not to forget, the making the distinction between creating a new table and altering an existing table is beyond the scope of this post.
Worth mentioning is that you could also use declarative SQL code with FOR XML to create the table structure. This is possible and would be able to generate the CREATE TABLE statements with a much better performance. From experience I know that this code will be much harder to maintain and you might run into the limitations of declarative SQL.
Is there any option to search inside cursor?
For example: I have a table(MyTable) with row number and value,
that I want to copy to another table(TestTable),
but let's say that if there was a value >= 5 then the next value,
that I want to copy should be <= 3.
I can use something like this:
create table TestTable
(row tinyint,
value tinyint)
declare #row tinyint, #value tinyint, #trigger bit
declare test_cursor cursor fast_forward for
select row,value from MyTable order by row
open test_cursor
fetch next from test_cursor into #row,#value
set #trigger = 0
while ##FETCH_STATUS = 0
begin
if #trigger = 0
begin
insert into TestTable values (#row,#value)
if #value >= 5 set #trigger = 1
end
else if #value <= 3
begin
insert into TestTable values (#row,#value)
set #trigger = 0
end
fetch next from test_cursor into #row,#value
end
close test_cursor
deallocate test_cursor
That will work, but my question is: is there an any way to search inside cursor
for the next falue that <= 3 once trigger = 1,
instead of fetching next row over and over every time?
No, cursors don't support the kind of querying that you're after. You will have to visit each value and check it in the loop.
The code posted here is 'example' code, it's not production code. I've done this to make the problem I'm explaining readable / concise.
Using code similar to that below, we're coming across a strange bug. After every INSERT the WHILE loop is stopped.
table containst 100 rows, when the insert is done after 50 rows then the cursor stops, having only touched the first 50 rows. When the insert is done after 55 it stops after 55, and so on.
-- This code is an hypothetical example written to express
-- an problem seen in production
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END
CLOSE MyCursor
DEALLOCATE MyCursor
There is an AFTER INSERT trigger on table2 which is used to log mutaties on table2 into an third table, aptly named mutations. This contains an cursor which inserts to handle the insert (mutations are logged per-column in an very specific manner, which requires the cursor).
A bit of background: this exists on an set of small support tables. It is an requirement for the project that every change made to the source data is logged, for auditing purposes. The tables with the logging contain things such as bank account numbers, into which vast sums of money will be deposited. There are maximum a few thousand records, and they should only be modified very rarely. The auditing functionality is there to discourage fraud: as we log 'what changed' with 'who did it'.
The obvious, fast and logical way to implement this would be to store the entire row each time an update is made. Then we wouldn't need the cursor, and it would perform an factor better. However the politics of the situation means my hands are tied.
Phew. Now back to the question.
Simplified version of the trigger (real version does an insert per column, and it also inserts the old value):
--This cursor is an hypothetical cursor written to express
--an problem seen in production.
--On UPDATE a new record must be added to table Mutaties for
--every row in every column in the database. This is required
--for auditing purposes.
--An set-based approach which stores the previous state of the row
--is expressly forbidden by the customer
DECLARE #col1 int
DECLARE #col2 int
DECLARE #col1_old int
DECLARE #col2_old int
--Loop through old values next to new values
DECLARE MyTriggerCursor CURSOR FAST_FORWARD FOR
SELECT i.col1, i.col2, d.col1 as col1_old, d.col2 as col2_old
FROM Inserted i
INNER JOIN Deleted d ON i.id=d.id
OPEN MyTriggerCursor
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
--Loop through all rows which were updated
WHILE(##FETCH_STATUS=0)
BEGIN
--In production code a few more details are logged, such as userid, times etc etc
--First column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col1', #col1, #col1_old)
--Second column
INSERT Mutaties (tablename, columnname, newvalue, oldvalue)
VALUES ('table2', 'col2', #col2, #col1_old)
FETCH NEXT FROM MyTriggerCursor INTO #col1, #col2, #col1_old, #col2_old
END
CLOSE MyTriggerCursor
DEALLOCATE MyTriggerCursor
Why is the code exiting in the middle of the loop?
Your problem is that you should NOT be using a cursor for this at all! This is the code for the example given above.
INSERT INTO table2(col1)
SELECT Col1 FROM table
where col1>10
You also should never ever use a cursor in a trigger, that will kill performance. If someone added 100,000 rows in an insert this could take minutes (or even hours) instead of millseconds or seconds. We replaced one here (that predated my coming to this job) and reduced an import to that table from 40 minites to 45 seconds.
Any production code that uses a cursor should be examined to replace it with correct set-based code. in my experience 90+% of all cursors can be reqwritten in a set-based fashion.
Ryan, your problem is that ##FETCH_STATUS is global to all cursors in an connection.
So the cursor within the trigger ends with an ##FETCH_STATUS of -1. When control returns to the code above, the last ##FETCH_STATUS was -1 so the cursor ends.
That's explained in the documentation, which can be found on MSDN here.
What you can do is use an local variable to store the ##FETCH_STATUS, and put that local variable in the loop. So you get something like this:
DECLARE #v1 int
DECLARE #v2 int
DECLARE #FetchStatus int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
WHILE(#FetchStatus=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
SET #FetchStatus = ##FETCH_STATUS
END
CLOSE MyCursor
DEALLOCATE MyCursor
It's worth noting that this behaviour does not apply to nested cursors. I've made an quick example, which on SqlServer 2008 returns the expected result (50).
USE AdventureWorks
GO
DECLARE #LocationId smallint
DECLARE #ProductId smallint
DECLARE #Counter int
SET #Counter=0
DECLARE MyFirstCursor CURSOR FOR
SELECT TOP 10 LocationId
FROM Production.Location
OPEN MyFirstCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
WHILE (##FETCH_STATUS=0)
BEGIN
DECLARE MySecondCursor CURSOR FOR
SELECT TOP 5 ProductID
FROM Production.Product
OPEN MySecondCursor
FETCH NEXT FROM MySecondCursor INTO #ProductId
WHILE(##FETCH_STATUS=0)
BEGIN
SET #Counter=#Counter+1
FETCH NEXT FROM MySecondCursor INTO #ProductId
END
CLOSE MySecondCursor
DEALLOCATE MySecondCursor
FETCH NEXT FROM MyFirstCursor INTO #LocationId
END
CLOSE MyFirstCursor
DEALLOCATE MyFirstCursor
--
--Against the initial version of AdventureWorks, counter should be 50.
--
IF(#Counter=50)
PRINT 'All is good with the world'
ELSE
PRINT 'Something''s wrong with the world today'
this is a simple misunderstanding of triggers... you don't need a cursor at all for this
if UPDATE(Col1)
begin
insert into mutaties
(
tablename,
columnname,
newvalue
)
select
'table2',
coalesce(d.Col1,''),
coalesce(i.Col1,''),
getdate()
from inserted i
join deleted d on i.ID=d.ID
and coalesce(d.Col1,-666)<>coalesce(i.Col1,-666)
end
basically what this code does is it checks to see if that column's data was updated. if it was, it compares the new and old data, and if it's different it inserts into your log table.
you're first code example could easily be replaced with something like this
insert into table2 (col1)
select Col2
from table
where Col1>10
This code does not fetch any further values from the cursor, nor does it increment any values. As it is, there is no reason to implement a cursor here.
Your entire code could be rewritten as:
DECLARE #v1 int
DECLARE #v2 int
SELECT #v1 = Col1, #v2 = Col2
FROM table
IF(#v1>10)
INSERT INTO table2(col1) VALUES (#v2)
Edit: Post has been edited to fix the problem I was referring to.
You do not have to use a cursor to insert each column as a separate row.
Here is an example:
INSERT LOG.DataChanges
SELECT
SchemaName = 'Schemaname',
TableName = 'TableName',
ColumnName = CASE ColumnID WHEN 1 THEN 'Column1' WHEN 2 THEN 'Column2' WHEN 3 THEN 'Column3' WHEN 4 THEN 'Column4' END
ID = Key1,
ID2 = Key2,
ID3 = Key3,
DataBefore = CASE ColumnID WHEN 1 THEN I.Column1 WHEN 2 THEN I.Column2 WHEN 3 THEN I.Column3 WHEN 4 THEN I.Column4 END,
DataAfter = CASE ColumnID WHEN 1 THEN D.Column1 WHEN 2 THEN D.Column2 WHEN 3 THEN D.Column3 WHEN 4 THEN D.Column4 END,
DateChange = GETDATE(),
USER = WhateverFunctionYouAreUsingForThis
FROM
Inserted I
FULL JOIN Deleted D ON I.Key1 = D.Key1 AND I.Key2 = D.Key2
CROSS JOIN (
SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) X (ColumnID)
In the X table, you could code additional behavior with a second column that specially describes how to handle just that column (let's say you wanted some to post all the time, but others only when the value changes). What's important is that this is an example of the cross join technique of splitting rows into each column, but there is a lot more that can be done. Note that the full join allows this to work on inserts and deletes as well as updates.
I also fully agree that storing each row is FAR superior. See this forum for more about this.
As ck mentioned, you are not fetching any further values. The ##FETCH_STATUS thus get's its value from your cursor contained in your AFTER INSERT trigger.
You should change your code to
DECLARE #v1 int
DECLARE #v2 int
DECLARE MyCursor CURSOR FAST_FORWARD FOR
SELECT Col1, Col2
FROM table
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #v1, #v2
WHILE(##FETCH_STATUS=0)
BEGIN
IF(#v1>10)
BEGIN
INSERT INTO table2(col1) VALUES (#v2)
END
FETCH NEXT FROM MyCursor INTO #v1, #v2
END