Row-level Triggers (SQL Server 2008 R2) - triggers

My trigger works fine if I insert 1 row, but not if I insert more than one. Working with SQL Server 2008 R2 and this is for a class project. Oracle has a FOR EACH ROW setting, but I have not found something similar in SQL Server...
CREATE TRIGGER tgRoom_HotelFloorName
ON Room
AFTER INSERT
AS
BEGIN
UPDATE Room
SET Room.HotelFloorName = (
SELECT HotelFloor.HotelFloorName FROM HotelFloor
JOIN Inserted
ON HotelFloor.HotelFloorID = Inserted.HotelFloorID
AND Room.RoomID = Inserted.RoomID);
END;
Here are the results of inserting 3 records; only the last one is updated. Based on the messages in SQL Server Management Studio, it looks like the other two were updated, but then got overwritten.
RoomID RoomName HotelFloorID HotelID HotelFloorName
1 San Diego A 12 2 NULL
2 San Diego B 12 2 NULL
3 Laguna 17 2 Lower Lobby Level, South Tower
Thanks for whatever wisdom you can provide.

Related

Convert differences between SQL Server 2008 R2 and 2012

I've got an issue while executing a SQL statement in SQL Server 2012 while it's perfectly working in a SQL Server 2008 R2...
The error message is :
Msg 8114, Level 16, State 5, Line 1
Error converting data type varchar
to numeric.
I want to execute this SQL Select statement :
Select
count(*)
from IMPORTBM
inner join ATTRIBUTE on ATT_ATTRIBUTE_ID = IMP_ATTRIBUTE_ID
where IMP_LOCATION_ID = 2
AND IMP_SERIAL_ID = 310001
AND IMP_VERSION_ID = 1
AND (
(ATT_ATTRIBUTE = 'PS_APISizing'
AND IMP_VALUE = 'C')
OR
(ATT_ATTRIBUTE = 'DTD'
AND ISNUMERIC(IMP_VALUE) = 1
AND CAST( IMP_VALUE as NUMERIC(38,19)) <= 0.469)
OR
(ATT_ATTRIBUTE = 'IOD'
AND ISNUMERIC(IMP_VALUE) = 1
AND CAST( IMP_VALUE as NUMERIC(38,19)) BETWEEN 3.684 AND 4.225)
)
Could you help me finding out why it's not working with SQL Server 2012 please ?
The problem is that conditions are not always evaluated in the order you place them. This means that Cast(IMP_VALUE as numeric(38, 19)) can fail when it is a non-numeric value, if the query execution plan happens to evaluate this before the IsNumeric(IMP_VALUE) = 1.
This is not determined by how many clauses there are or what version of SQL Server you're running--those are only coincidences. The problem is exactly as I described. The fact that you get different results with different queries is due to different queries, or different servers, using different execution plans.
The cure is to make sure that all conditions will not throw an error, no matter what order they are executed in.
For all versions of SQL Server, you can do this:
Convert(
numeric(38,19),
CASE WHEN IsNumeric(IMP_VALUE) = 1 THEN IMP_VALUE ELSE NULL END
) <= 0.469
Or, in SQL Server 2012 and up, you can use Try_Convert:
Try_Convert(numeric(38,19), IMP_VALUE) <= 0.469
If your server complains that this is not a built-in function name, then it is likely that your database was upgraded from a prior version and you need to alter the database compatibility level. Note that doing this could have some other effects in the system that you need to consider, so study up on it first, and try it out in a non-production system before doing it in production (or do it during a non-critical period and set the compatibility level back if you run into any issues). The danger is not that any damage will occur, but that query results could be different or errors could occur (different ordering of query or stored procedure results being a more common occurrence).
ALTER DATABASE database_name
SET COMPATIBILITY_LEVEL = 110; -- SQL Server 2012
What is the definition of the table? (IMP_VALUE at least). Why not store numeric in the 1st place...
2008 and 2012 query plan probably differ and does not handle the parameter and condition in the same order.
With SQL Server 2012 (compatibility level 110) you can try to replace the 2 cast:
AND ISNUMERIC(IMP_VALUE) = 1
AND CAST( IMP_VALUE as NUMERIC(38,19)) <= 0.469)
by
AND TRY_CONVERT(NUMERIC(38,19), IMP_VALUE) <= 0.469
AND TRY_CONVERT(NUMERIC(38,19), IMP_VALUE) BETWEEN 3.684 AND 4.225
It returns NULL without error when it cannot convert it.

Postgresql update 2 tables in one update statement

I have two different tabs with same field, like:
host.enabled, service.enabled.
How I can update his from 1 update statement?
I tried:
UPDATE hosts AS h, services AS s SET h.enabled=0, s.enabled=0
WHERE
ctid IN (
SELECT hst.ctid FROM hosts hst LEFT JOIN services srv ON hst.host_id = srv.host_id
WHERE hst.instance_id=1
);
On mysql syntax this query worked like this:
UPDATE hosts LEFT JOIN services ON hosts.host_id=services.host_id SET hosts.enabled=0, services.enabled=0 WHERE hosts.instance_id=1
I didn't really understand your schema. If you can set up a fiddle that would be great.
In general though to update two tables in a single query the options are:
Trigger
This makes the assumption that you always want to update both together.
Stored procedure/function
So you'll be doing it as multiple queries in the function, but it can be triggered by a single SQL statement from the client.
Postgres CTE extension
Postgres permits common table expressions (with clauses) to utilise data manipulation.
with changed_hosts as (
update hosts set enabled = true
where host_id = 2
returning *
)
update services set enabled = true
where host_id in (select host_id from changed_hosts);
In this case the update in the WITH statement runs and sets the flag on the hosts table, then the main update runs, which updates the records in the services table.
SQL Fiddle for this at http://sqlfiddle.com/#!15/fa4d3/1
In general though, its probably easiest and most readable just to do 2 updates wrapped in a transaction.

Query referencing aliased column in order by in SQLServer 2012

I have a SQL Select query that's embedded in a piece of C# code which I don't want to change. My problem is that the query executes fine on SQLServer 2008 but not 2012.
The offending line of code is:
Select code as SiteCode from TimeSheetContracts S order by S.SiteCode
Executed in a database on SQL2008 it works fine. The same database upgraded to SQLServer 2012 errors with the following...
Msg 207, Level 16, State 1, Line 2
Invalid column name 'SiteCode'.
If I edit the query to be
Select code as SiteCode from TimeSheetContracts S order by SiteCode
it works fine. Can anyone explain this?
There is no column in TimeSheetContracts called SiteCode, so a reference to s.SiteCode is not valid. Aliasing in ORDER BY is a little more strict since SQL Server 2000, when the syntax was a little more forgiving. The only way s.SiteCode would have worked on your SQL Server 2008 instance was if your database was in COMPATIBILITY_LEVEL = 80 (go ahead and try it on a different database that is 90 or greater). Once you move to SQL Server 2012, 80 is no longer an option. On a 2005, 2008 or 2008 R2 instance, try this:
CREATE DATABASE floob;
GO
USE floob;
GO
CREATE TABLE dbo.SalesOrderHeader(SalesOrderID INT);
GO
SELECT SalesOrderID AS ID FROM dbo.SalesOrderHeader AS h ORDER BY h.ID; -- fails
GO
ALTER DATABASE floob SET COMPATIBILITY_LEVEL = 80;
GO
SELECT SalesOrderID AS ID FROM dbo.SalesOrderHeader AS h ORDER BY h.ID; -- works
GO
USE master;
GO
DROP DATABASE floob;
If you want to use the column alias, you'll need to (and should always have been) just use the alias. If you want to use the table alias prefix, you'll need to use s.code.

SSRS 2005 passing parameters to SQL Server 2000 stored procedure

Below is code that I built from an example I found online, I can't find the link, but the code is referenced in the answers on this stack overflow question: Passing multiple values for a single parameter in Reporting Services.
Here is the SQL code I am working with right now within my stored procedure, it was a long procedure so I summed it down to just the section I am working on, and added the DECLARE and SET for #EMPLOYEES, which are passed as a parameter from SSRS to make the code snippet run.
DECLARE #EMPLOYEES varchar(8000)
-- EMPLOYEES is a comma separated list of EMPLOYEE IDS
-- FROM SSRS Report Parameters. Each ID is 12 characters
-- And there are 806 Employees to choose from, which
-- when all are selected, the Comma separated string grows
-- to 11,193 characters, much longer than 8000
SET #EMPLOYEES = 'EMP000000001,EMP000000002,EMP000000003'
CREATE TABLE #EMPLOYEEIDS
(
EMPLOYEEID varchar(100) NOT NULL
)
DECLARE #CharIndex AS int
DECLARE #Piece AS varchar(100)
-- FILL THE #EMPLOYEEIDS TABLE WITH THE COMMA SEPARATED EMPLOYEE IDS
SELECT #CharIndex = 1
WHILE #CharIndex > 0 AND LEN(#EMPLOYEES) > 0
BEGIN
SELECT #CharIndex = CHARINDEX(',', #EMPLOYEES)
IF #CharIndex > 0
SELECT #Piece = LEFT(#EMPLOYEES, #CharIndex - 1)
ELSE
SELECT #Piece = #EMPLOYEES
INSERT INTO #EMPLOYEEIDS (EMPLOYEEID) VALUES (#Piece)
SELECT #EMPLOYEES = RIGHT(#EMPLOYEES, LEN(#EMPLOYEES) - #CharIndex)
END
SELECT * FROM #EMPLOYEEIDS
DROP TABLE #EMPLOYEEIDS
I had 6 sets of multi-values, all of them worked fine, until I found that the reports were missing much of the data for employees, to which I found that the VARCHAR(8000) was overflowed when selecting all the employees on the report parameters (there are over 800 of them). The Report would run, SQL would happily truncate the VARCHAR to 8000 characters, and a quarter of the IDS were not parsed.
So I tried to switch the VARCHAR to a text field, and none of the parsing functions would work when the field is set up as TEXT. I get errors like the following:
Msg 8116, Level 16, State 2, Procedure usp_QualityMonitoring_AllProfiles_SelectWithParameters, Line 89
Argument data type text is invalid for argument 1 of left function.
This is understandable, I know that many functions that work with VARCHAR will not work with TEXT. So, SQL is truncating everything after 8000 characters when I use a VARCHAR, and the procedure won't ever run if I switch it to TEXT.
What other options to I have to pass multi-valued parameters from SSRS to a SQL Server stored procedure that can support this many options?
OR is there a way to fix the code in the stored procedure to parse through TEXT instead of VARCHAR?
Note: I originally thought the SQL Server running the Stored Proc was 2005, but I have determined that it is not:
SELECT ##VERSION
-- Microsoft SQL Server 2000 - 8.00.2039 (Intel X86) May 3 2005 23:18:38 Copyright (c) 1988-2003 Microsoft Corporation Standard Edition on Windows NT 5.2 (Build 3790: Service Pack 2)

OptimisticConcurrencyException -- SQL 2008 R2 Instead of Insert Trigger with Entity Framework

Using a SQL 2008 R2 November release database and a .net 4.0 Beta 2 Azure worker role application. The worker role collects data and inserts it into a single SQL table with one identity column. Because there will likely be multiple instances of this worker role running, I created an Insert Instead Of trigger on the SQL table. The trigger performs Upsert functionality using the SQL Merge function. Using T-SQL I was able to verify the insert instead of trigger functions correctly, new rows were inserted while existing rows were updated.
This is the code for my trigger:
Create Trigger [dbo].[trgInsteadOfInsert] on [dbo].[Cars] Instead of Insert
as
begin
set nocount On
merge into Cars as Target
using inserted as Source
on Target.id=Source.id AND target.Manufactureid=source.Manufactureid
when matched then
update set Target.Model=Source.Model,
Target.NumDoors = Source.NumDoors,
Target.Description = Source.Description,
Target.LastUpdateTime = Source.LastUpdateTime,
Target.EngineSize = Source.EngineSize
when not matched then
INSERT ([Manufactureid]
,[Model]
,[NumDoors]
,[Description]
,[ID]
,[LastUpdateTime]
,[EngineSize])
VALUES
(Source.Manufactureid,
Source.Model,
Source.NumDoors,
Source.Description,
Source.ID,
Source.LastUpdateTime,
Source.EngineSize);
End
Within the worker role I am using Entity Framework for an object model. When I call the SaveChanges method I receieve the following exception:
OptimisticConcurrencyException
Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. Refresh ObjectStateManager entries.
I understand this is likly due to SQL not reporting back an IdentityScope for each new inserted/updated row. Then EF thinks the rows were not inserted and the transaction is not ultimately not committed.
What is the best way to handle this exception? Maybe using OUTPUT from the SQL merge function?
Thanks!
-Paul
As you suspected, the problem is that any insertions into a table with an Identity column are immediately followed by a select of the scope_identity() to populate the associated value in the Entity Framework. The instead of trigger causes this second step to be missed, which leads to the 0 rows inserted error.
I found an answer in this StackOverflow thread that suggested adding the following line at the end of your trigger (in the case where the item is not matched and the Insert is performed).
select [Id] from [dbo].[TableXXX] where ##ROWCOUNT > 0 and [Id] = scope_identity()
I tested this with Entity Framework 4.1, and it solved the problem for me. I have copied my entire trigger creation here for completeness. With this trigger defenition I was able to add rows to the table by adding Address entities to the context and saving them using context.SaveChanges().
ALTER TRIGGER [dbo].[CalcGeoLoc]
ON [dbo].[Address]
INSTEAD OF INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT OFF;
-- Insert statements for trigger here
INSERT INTO Address (Street, Street2, City, StateProvince, PostalCode, Latitude, Longitude, GeoLoc, Name)
SELECT Street, Street2, City, StateProvince, PostalCode, Latitude, Longitude, geography::Point(Latitude, Longitude, 4326), Name
FROM Inserted;
select AddressId from [dbo].Address where ##ROWCOUNT > 0 and AddressId = scope_identity();
END
I had almost exactly the same scenario: Entity Framework-driven inserts to a view with an INSTEAD OF INSERT trigger on it were resulting in the "...unexpected number of rows (0)..." exception. Thanks to Ryan Gross's answer I fixed it by adding
SELECT SCOPE_IDENTITY() AS CentrePersonID;
at the end of my trigger, where CentrePersonID is the name of the key field of the underlying table that has an auto-inrcementing identity. This way the EF can discover the ID of the newly-inserted record.