Resolve the error: The multi-part identifier could not be bound? - sql

I am creating a stored procedure and the final "When not matched" statement is throwing an error for the tmp.DelDate and tmp.DelUser fields. The "tmp" table is a User-Defined Table Type and the definition is below the sp code. 99 times out of 100, the problem is a bad alias or other typo. I've been staring at this and I have to be missing something small. This last statement is almost identical to the first "When Matched" statement.
ALTER Procedure dbo.spInsertUpdateProtocolRiskStrats
#riskStratsTable ProtocolRiskStrats READONLY
WITH RECOMPILE
AS
BEGIN
WITH riskStrats as (
SELECT ol.StratId,
ol.LinkType,
ol.LinkId,
ol.add_user,
ol.add_date,
ol.del_user,
ol.del_date
FROM ots_StratTriggerOutcomesLinks ol
JOIN #riskStratsTable rst on ol.LinkId = rst.LinkId
WHERE ol.LinkId = rst.LinkId
AND ol.LinkType = 2
)
MERGE riskStrats
USING #riskStratsTable as tmp
ON riskStrats.LinkId = tmp.LinkId
WHEN MATCHED THEN
UPDATE SET riskStrats.add_date = tmp.AddDate,
riskStrats.add_user = tmp.AddUser,
del_date = null,
del_user= null
WHEN NOT MATCHED THEN
INSERT (StratId, LinkType, LinkId, add_user, add_date)
VALUES (tmp.StratId, tmp.LinkType, tmp.LinkId, tmp.AddUser, tmp.AddDate)
WHEN NOT MATCHED BY SOURCE THEN
UPDATE SET riskStrats.del_date = tmp.DelDate,
riskStrats.del_user = tmp.DelUser;
END
User Table definition
CREATE TYPE dbo.ProtocolRiskStrats AS TABLE
(
KeyId int null,
StratId int null,
LinkType int null,
LinkId int null,
AddUser int null,
AddDate datetime null,
DelUser int null,
DelDate datetime null
)

As noted by #AlwaysLearning, I was assigning values that couldn't exist because it was a "not matched" condition. I updated my last statement to use constant values. I had to add another parameter to pass in user name. I could have also done a "Top 1" on my TVP but my dev lead didn't like that.
UPDATE SET riskStrats.del_date = GETDATE(),
riskStrats.del_user = #userName;

Related

Insert new record into autonumbered table, and then use the autonumber in another table

I'm writing a stored procedure to insert data from a form into two tables. One table has an autonumbered identity field. I need to insert the data into that table, find the newly created autonumber, and use that number to insert data into another table. So, to boil it down, I have a one-to-many link between the two tables and I need to make sure the identity field gets inserted.
Is this code the best way to do something like this, or am I missing something obvious?
CREATE PROCEDURE [dbo].[sp_Insert_CRT]
(
#TRACKING_ID int,
#CUST_NUM int,
#TRACKING_ITEM_ID int,
#STATEMENT_NUM nvarchar (200) = null,
#AMOUNT numeric (15, 2),
#BBL_ADJUSTED int = NULL,
#PAID_VS_BILLED int = NULL,
#ADJUSTMENT_TYPE int = NULL,
#ENTERED_BY nvarchar (10) = NULL,
#ENTERED_DATE date = NULL,
#AA_STATUS int = NULL
)
AS
BEGIN
-- Insert data into CRT_Main, where Tracking_ID is an autonumber field
INSERT into tbl_CRT_Main
(
-- TRACKING_ID
CUST_NUM
,TRACKING_ITEM_ID
,STATEMENT_NUM
,AMOUNT
)
VALUES
(
-- #TRACKING_ID
#CUST_NUM
,#TRACKING_ITEM_ID
,#STATEMENT_NUM
,#AMOUNT
)
-- Find the newly generated autonumber, and use it in another table
BEGIN TRANSACTION
DECLARE #TrackID int;
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
COMMIT
INSERT into tbl_CRT_Admin_Adjustment
(
TRACKING_ID
,BBL_ADJUSTED
,PAID_VS_BILLED
,[ADJUSTMENT_TYPE]
,[ENTERED_BY]
,[ENTERED_DATE]
,AA_STATUS
)
VALUES
(
#TrackID
,#BBL_ADJUSTED
,#PAID_VS_BILLED
,#ADJUSTMENT_TYPE
,#ENTERED_BY
,#ENTERED_DATE
,#AA_STATUS
)
END
SELECT #TrackID = coalesce((select max(TRACKING_ID) from tbl_CRT_Main), 1)
No, don't do this. This will get you the maximum value of TRACKING_ID yes, but that doesn't mean that's the value that was created for your INSERT. If multiple INSERT statements were being run by different connections then very likely you would get the wrong value.
Instead, use SCOPE_IDENTITY to get the value:
SET #TrackID = SCOPE_IDENTITY();
Also, there is no need to wrap the above in an explicit transaction like you have with your SELECT MAX(). Instead, most likely, the entire batch in the procedure should be inside it's own explicit transaction, with a TRY...CATCH so that you can ROLLBACK the whole batch in the event of an error.

Merge using Multiple table sources (User Defined Table Type & Input Parameters)

I'm trying to upsert data into a table, called "CustomOrderReqs". I'll insert/update data into this table periodically using User-Defined Table Type & Input Parameters to the Stored proc. I have the final table as follows:
CustomOrderReqs
I have defined a User-Defined Table type to be use in the Upsert Stored Proc, as follows
CREATE TYPE [dbo].[CustomOrderRequestsType] AS TABLE(
[Customer_ID] [varchar](50) NOT NULL,
[Customer_Email] [varchar](50) NOT NULL,
[Customer_Notes] [nvarchar](Max) NOT NULL ) Go
Below is my stored procedure to perform this operation, and it has some errors when I'm trying to use the User-Defined table type and Input parameters, along in the Merge statement.
ALTER PROCEDURE [dbo].[sp_UpsertCustomDesignReqsTable]
#OrderNumber VARCHAR(30)
,#Product_Id VARCHAR(50)
,#Purchase_amt VARCHAR(Max)
,#Details CustomOrderRequestsType READONLY
AS
BEGIN
SET NOCOUNT ON;
MERGE [dbo].[CustomOrderReqs] AS TARGET
USING (VALUES (#OrderNumber, #Product_Id, ,#Purchase_amt, #Details) ) AS SOURCE /*** Error Here **/
ON (SOURCE.[#OrderNumber] = TARGET.[Order Number])
AND (SOURCE.[#Product_Id] = TARGET.[Product_Id]
AND (SOURCE.[Cutomer_Id] = TARGET.[Customer_Id]))
WHEN MATCHED THEN
UPDATE SET TARGET.[Order Number] = #OrderNumber,
TARGET.[Product_Id] = #NegItem,
TARGET.[Purchase_amt] = #Purchase_amt,
TARGET.[Customer_ID] = Source.[Customer_ID],
TARGET.[Customer_Email = Source.[Customer_Email,
TARGET.[Customer_Notes] = Source.[Customer_Notes]
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES (
#OrderNumber,
#NegItem,
#Purchase_amt,
Source.[Customer_ID],
Source.[Customer_Email,
Source.[Customer_Notes]);
END
With the above query, I'm getting an error message on the Source Declaration line and the message is as follows:
Msg 137, Level 16, State 1, Procedure sp_UpsertCustomDesignReqsTable, Line 21 [Batch Start Line 7]
Must declare the scalar variable "#Details"
Any suggestions will be greatly appreciated.
What add the variables to the merge table? Why not just use them directly.
Also you might find things easier if you solve all your syntax errors first, including defining #NegItem.
Its also best practice to explicitly list the columns when inserting.
MERGE [dbo].[CustomOrderReqs] AS [TARGET]
USING #Details AS [SOURCE]
ON [SOURCE].OrderNumber = #OrderNumber
AND [SOURCE].Product_Id = #Product_Id
AND [SOURCE].Cutomer_Id = [TARGET].Customer_Id
WHEN MATCHED THEN
UPDATE SET
[TARGET].Customer_ID = [SOURCE].Customer_ID
, [TARGET].Customer_Email = [SOURCE].Customer_Email
, [TARGET].Customer_Notes = [SOURCE].Customer_Notes
WHEN NOT MATCHED BY TARGET THEN
INSERT (OrderNumber, ProductId, Amount, Customer_Id, Customer_Email, Customer_Notes)
VALUES (#OrderNumber, #Product_Id, #Purchase_amt, [SOURCE].[Customer_ID], [SOURCE].[Customer_Email, [SOURCE].[Customer_Notes]);
As you have added a further requirement in the comments to only update when a value has changed consider changing the line WHEN MATCHED THEN to
WHEN MATCHED THEN AND (
-- Assume ID is never null
[SOURCE].Customer_ID <> [TARGET].Customer_ID
-- Handle null
OR COALESCE([SOURCE].Customer_Email,'') <> COALESCE([TARGET].Customer_Email,'')
-- Handle null
OR COALESCE([SOURCE].Customer_Notes,'') <> COALESCE([TARGET].Customer_Notes,'')
)
However I would add duplicate detection in the logging trigger rather than here.
Note: The Official Docs explain all this and more.

SQL Check Constraints apply on all rows

So I've got this table where I keep track of Items, there are 3 kinds of items:
Weapon
Armour
Potion
I created 3 check constraints for those:
CK_Weapon: CHECK ([Type]=(1) AND NOT ([PhysDamage]+[ElemDamage])<(1) AND [AttackSpeed]>(0.5))
CK_Armour: CHECK ([Type]=(2) AND NOT ([PhysReduction]+[ElemReduction]<(1)))
CK_Potion: ([Type]=(3) AND ([PhysDamage]+[ElemDamage])=(0) AND [AttackSpeed]=(0) AND ([PhysReduction]+[ElemReduction])=(0));
When I try to add a potion with the following insert;
DECLARE #num int, #Type int, #Name varchar(50), #Description varchar(50), #Gold int
SET #Type = 3
SET #Name = 'Spirit Potion'
SET #Description = 'Restores a bit of Spirit'
SET #Gold = 150
insert into Item(Type, Name, Description, GoldValue) VALUES(#Type, #Name, #Description, #Gold)
I get the following error:
The INSERT statement conflicted with the CHECK constraint "CK_Weapon". The conflict occurred in database "Database", table "dbo.Item".
But it shouldn't trigger this CHECK at all, because Potion Type should be 3!
Is there an easy way for me to alter those CHECKs so it'll only trigger when the Type is the same?
You need to reverse the first part of your checks so that they give a "pass" to rows they don't care about. So, e.g. for the Armour check, you should check that either the Type isn't 2 (so this check constraint doesn't care) Or that (the checks that apply to armour) are passed:
CHECK ([Type]!=(2) OR (NOT ([PhysReduction]+[ElemReduction]<(1))))
Repeat for your other checks. At the moment, you cannot insert any rows since the combination of check constraints require that Type be simultaneously equal to 1, 2 and 3.
You are trying to put 3 constraints onto the same column, hoping that it will only trigger one of them, matching the Type you are inputting. But it will check them all, that's why the CK_Weapon constraint is violated as it is expecting Type = 1.
You might want to try to write a bit of case-logic inside your constraint, like this:
create table [RPGInventory]
(
[Type] tinyint not null
, [PhysDamage] int null
, [ElemDamage] int null
, [AttackSpeed] int null
, [PhysReduction] int null
, [ElemReduction] int null
, constraint ckInventoryType check (1 = iif([Type] = (1)
and not ([PhysDamage] + [ElemDamage]) < (1)
and [AttackSpeed] > (0.5), 1
, iif([Type] = (2)
and not ([PhysReduction] + [ElemReduction] < (1)), 1
, iif([Type] = (3)
and ([PhysDamage] + [ElemDamage]) = (0)
and [AttackSpeed] = (0)
and ([PhysReduction] + [ElemReduction]) = (0), 1, 0)))
)
)
go

How to construct Sql Server TVP with a non default order of columns?

My scenario is simple - I create a TVP with an IEnumerable<SqlDataRecord> as its value and execute the query using it.
The results I get back do not make any sense. So, I wrote a TraceSql function that dumps the SQL along with its parameters in a way ready to be copied and pasted into the SSMS.
I was puzzled to discover, that the SQL displayed by my function produces the correct results, when pasted to SSMS and ran there.
Finally, I started Sql Server profiler and found out the source of the mystery.
Here is the SQL produced by my TraceSql function:
DECLARE #AdmSiteId Int = 93
DECLARE #src dbo.TableOfObjectChecksum2
INSERT INTO #src (Id,Checksum,AuxId) VALUES
(8395,258295360,1)
,(8395,1114574098,2)
,(8395,-19039848,3)
,(8395,337145572,4)
,(8395,1083939112,5)
SELECT ISNULL(src.Id, dst.ClientId) Id, ISNULL(src.AuxId, dst.ClientLegalId) AuxId,
CASE
WHEN src.Checksum IS NULL THEN 0
WHEN dst.Checksum IS NOT NULL THEN 1
ELSE 2
END Checksum
FROM #src src
FULL JOIN AdmCustomerInfoLegal dst ON src.Id = dst.ClientId AND src.AuxId = dst.ClientLegalId
LEFT JOIN AdmClientSite cs ON cs.AdmClientMasterId = dst.ClientId
WHERE (cs.AdmSiteId = #AdmSiteId OR cs.AdmSiteId IS NULL) AND (src.Checksum IS NULL OR dst.Checksum IS NULL OR src.Checksum <> dst.Checksum)
ORDER BY Checksum, Id, AuxId
Now here is the SQL as captured by the profiler:
declare #p4 dbo.TableOfObjectChecksum2
insert into #p4 values(8395,258295360,1)
insert into #p4 values(8395,1114574098,2)
insert into #p4 values(8395,-19039848,3)
insert into #p4 values(8395,337145572,4)
insert into #p4 values(8395,1083939112,5)
exec sp_executesql N'
SELECT ISNULL(src.Id, dst.ClientId) Id, ISNULL(src.AuxId, dst.ClientLegalId) AuxId,
CASE
WHEN src.Checksum IS NULL THEN 0
WHEN dst.Checksum IS NOT NULL THEN 1
ELSE 2
END Checksum
FROM #src src
FULL JOIN AdmCustomerInfoLegal dst ON src.Id = dst.ClientId AND src.AuxId = dst.ClientLegalId
LEFT JOIN AdmClientSite cs ON cs.AdmClientMasterId = dst.ClientId
WHERE (cs.AdmSiteId = #AdmSiteId OR cs.AdmSiteId IS NULL) AND (src.Checksum IS NULL OR dst.Checksum IS NULL OR src.Checksum <> dst.Checksum)
ORDER BY Checksum, Id, AuxId',N'#AdmSiteId int,#src [dbo].[TableOfObjectChecksum2] READONLY',#AdmSiteId=93,#src=#p4
Notice the INSERT statements in the SQL captured by the profiler do not specify the column list. However, when I create the TVP I specify a different order of the columns.
Here is the schema of the dbo.TableOfObjectChecksum2 table UDT:
CREATE TYPE [dbo].[TableOfObjectChecksum2] AS TABLE(
[Id] [int] NOT NULL,
[AuxId] [int] NOT NULL,
[Checksum] [int] NOT NULL,
PRIMARY KEY ([Id],[AuxId])
)
Here is how I create the respective TVP in C#:
var items = GetItemsSomehow();
var metadata = new[]
{
new SqlMetaData("Id", SqlDbType.Int),
new SqlMetaData("Checksum", SqlDbType.Int),
new SqlMetaData("AuxId", SqlDbType.Int)
};
new SqlParameter("src", SqlDbType.Structured)
{
TypeName = "dbo.TableOfObjectChecksum2",
SqlValue = items.Select(x =>
{
var rec = new SqlDataRecord(metadata);
rec.SetInt32(0, x.ClientId);
rec.SetInt32(1, x.Checksum);
rec.SetInt32(2, x.ClientLegalId);
return rec;
}),
};
I thought that the column order I specified in the metadata array defines the matching between the ordinals and the fields. But it is not.
Now I understand that the best is to change my TVP to pass the columns in the same order as defined in the database. However, out of curiousity, how am I to create a TVP with a different column order, like in my example (which does not work)?
EDIT
My English is obviously inadequate to explain myself.
Please, examine the SqlMetadata[] argument passed to SqlDataRecord constructor. Does the order of the items matter? I thought it does. So, if I pass
new[]
{
new SqlMetaData("Id", SqlDbType.Int),
new SqlMetaData("Checksum", SqlDbType.Int),
new SqlMetaData("AuxId", SqlDbType.Int)
};
then it means something like this "In the batch insert SQL statement specify the columns in the order Id,Checksum,AuxId". In other words, I thought that the batch insert statement generated by ADO.NET given the aforementioned metadata is something like this:
INSERT INTO WhatEver (Id,Checksum,AuxId) VALUES (...),(...),...,(...)
I think it is legitimate to have assumed such a logic. Given this assumption the code
rec.SetInt32(0, x.ClientId);
rec.SetInt32(1, x.Checksum);
rec.SetInt32(2, x.ClientLegalId);
is absolutely valid, because:
ordinal 0 is Id metadata - given ClientId
ordinal 1 is Checksum metadata - given Checksum
ordinal 2 is AuxId metadata - given ClientLegalId
However, what I have stumbled upon is that the order of the metadata items does not matter to ADO.NET internals, because the insert statement actually produced by it is something like this:
INSERT INTO WhatEver VALUES (...)
INSERT INTO WhatEver VALUES (...)
...
INSERT INTO WhatEver VALUES (...)
Meaning, no matter how I order the metadata items, the actual insert SQL goes by the order of the columns in the database schema. And of course, the code
rec.SetInt32(0, x.ClientId);
rec.SetInt32(1, x.Checksum);
rec.SetInt32(2, x.ClientLegalId);
becomes invalid, because in the database:
ordinal 1 corresponds to AuxId and now it is given the Checksum
ordinal 2 corresponds to Checksum and now it is given the ClientLegalId
So, my assumption is wrong.
Now my question - is it still possible to batch insert using a collection of SqlDataRecord objects and have a custom order of the columns? Or pass only the required columns, relying on the defaults and NULLs to kick in? Because an insert SQL without giving an explicit column list must provide the values for each and every column, even the nullable and default ones. Otherwise Sql Server spits out Column name or number of supplied values does not match table definition.
I'm experiencing this issue and unfortunately there's no way to ignore the order of columns. The only minor improvement I was able to do was strong-typing:
var record = new SqlDataRecord(
new SqlMetaData(nameof(x.Id), SqlDbType.Int),
new SqlMetaData(nameof(x.Checksum), SqlDbType.Int),
new SqlMetaData(x.nameof(x.AuxId), SqlDbType.Int));
var index = record.GetOrdinal(nameof(x.Id));
record.SetInt32(index, x.Id);
index = record.GetOrdinal(nameof(x.Checksum));
record.SetInt32(index, x.Checksum);
index = record.GetOrdinal(nameof(x.AuxId));
record.SetInt32(index, x.AuxId);
Still the order matters and the name of columns and entities' property name should match.
But the select does not match.
And why inconsistent names?
rec.SetInt32(0, x.ClientId);
rec.SetInt32(1, x.Checksum);
rec.SetInt32(2, x.ClientLegalId);
CREATE TYPE [dbo].[TableOfObjectChecksum2] AS TABLE(
[Id] [int] NOT NULL,
[AuxId] [int] NOT NULL,
[Checksum] [int] NOT NULL,
PRIMARY KEY ([Id],[AuxId])
)

Bit parameter with Null value in Stored Procedure

I'm having a bit value in my table, which contains bit (0 or 1) and NULL (as default).
Here is my SProc:
CREATE PROCEDURE msp_CustomerStatistics
#Postal_MinValue int,
#Postal_MaxValue int,
#SubscriberState bit,
#CustomerType varchar(50)
BEGIN
[...]
WHERE Sub = #SubscriberState
AND Postal BETWEEN #Postal_MinValue AND #Postal_MaxValue
AND CustType = #CustomerType
END
When I pass the #SubscriberState parameter with 1 or 0, the result is correct.
But when I pass null, the result is 0, which ain't correct.
If I create a SQL select with following where clause:
WHERE Sub IS NULL
Then the result shows the correct count.
Any idea how I make my Stored Procedure working with NULL parameter in my WHERE clause too??
You can not use the = operator with null values. Comparisons with NULL always return false. Try to modify your WHERE statement to the following:
WHERE (Sub = #SubscriberState OR (#SubscriberState IS NULL AND Sub IS NULL))
You could either set null values to 0 and check it like this:
WHERE Isnull(Sub,0) = #SubscriberState
or have a tri-state sort of bodge like:
WHERE Isnull(Sub,3) = isnull(#SubscriberState,3)