I created a SqlDependency so that an event would fire when the results of a particular query change.
// Create a command
SqlConnection conn = new SqlConnection(connectionString);
string query = "SELECT MyColumn FROM MyTable;";
SqlCommand cmd = new SqlCommand(query, conn)
cmd.CommandType = CommandType.Text;
// Register a dependency
SqlDependency dependency = new SqlDependency(cmd);
dependency.OnChange += DependencyOnChange;
When this code executes, a stored procedure is automatically created with a name like
SqlQueryNotificationStoredProcedure-82ae1b92-21c5-46ae-a2a1-511c4f849f76
This procedure is unencrypted, which violates requirements I have been given. I have two options:
Convince the customer that it doesn't matter that the auto generated procedure is unencrypted because it only does cleanup work and contains no real information (thanks to ScottChamberlain for pointing this out).
Find a way to encrypt the stored procedure generated by SqlDependency.
How can I accomplish option 2?
Contents of the stored procedure in question:
CREATE PROCEDURE [dbo].[SqlQueryNotificationStoredProcedure-b124707b-23fc-4002-aac3-4d52a71c5d6b]
AS
BEGIN
BEGIN TRANSACTION;
RECEIVE TOP (0) conversation_handle
FROM [SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b];
IF (
SELECT COUNT(*)
FROM [SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b]
WHERE message_type_name = 'http://schemas.microsoft.com/SQL/ServiceBroker/DialogTimer'
) > 0
BEGIN
IF (
(
SELECT COUNT(*)
FROM sys.services
WHERE NAME = 'SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b'
) > 0
)
DROP SERVICE [SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b];
IF (OBJECT_ID('SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b', 'SQ') IS NOT NULL)
DROP QUEUE [SqlQueryNotificationService-b124707b-23fc-4002-aac3-4d52a71c5d6b];
DROP PROCEDURE [SqlQueryNotificationStoredProcedure-b124707b-23fc-4002-aac3-4d52a71c5d6b];
END
COMMIT TRANSACTION;
END
GO
Create a DDL trigger that checks if a procedure with a name like "SqlQueryNotificationStoredProcedure-" is being created, and if so, immediately alter it WITH ENCRYPTION instead:
CREATE TRIGGER [TR_EncryptQueryNotificationProcedures]
ON DATABASE
AFTER CREATE_PROCEDURE, ALTER_PROCEDURE
AS
BEGIN
SET ARITHABORT ON;
SET NOCOUNT ON;
IF TRIGGER_NESTLEVEL() > 1 RETURN;
-- For debugging purposes only
PRINT CONVERT(NVARCHAR(MAX), EVENTDATA());
DECLARE #DatabaseName NVARCHAR(128);
SET #DatabaseName = EVENTDATA().value(
'(/EVENT_INSTANCE/DatabaseName)[1]', 'NVARCHAR(128)'
);
DECLARE #Schema NVARCHAR(128);
SET #Schema = EVENTDATA().value(
'(/EVENT_INSTANCE/SchemaName)[1]', 'NVARCHAR(128)'
);
DECLARE #Name NVARCHAR(128);
SET #Name = EVENTDATA().value(
'(/EVENT_INSTANCE/ObjectName)[1]', 'NVARCHAR(128)'
);
DECLARE #Definition NVARCHAR(MAX);
SELECT #Definition =
OBJECT_DEFINITION(
OBJECT_ID(
QUOTENAME(#DatabaseName) + '.' +
QUOTENAME(#Schema) + '.' +
QUOTENAME(#Name),
'P'
)
)
;
-- If the sproc is already encrypted, we can't do anything with it
IF #Definition IS NULL RETURN;
SELECT #Definition = STUFF(
#Definition,
CHARINDEX('CREATE', #Definition),
LEN('CREATE'),
'ALTER'
);
IF
#Name LIKE 'SqlQueryNotificationStoredProcedure-%' AND
-- this should always be false since we can't read encrypted definitions,
-- but just to make sure
#Definition NOT LIKE '%WITH ENCRYPTION AS BEGIN%'
BEGIN;
SET #Definition = REPLACE(
#Definition, 'AS' + CHAR(13) + CHAR(10) + 'BEGIN',
'WITH ENCRYPTION AS BEGIN'
);
EXEC (#Definition);
END;
END;
GO
ENABLE TRIGGER [TR_EncryptQueryNotificationProcedures] ON DATABASE;
Disclaimer: not tested against an actual dependency notification, but the basic idea is sound. It's quite brittle because it depends on the exact form of the procedure, of course -- making it more robust is possible, but tedious.
Related
SQL Server is 2014. Visual Studio 2017.
I am trying to pass 3 parameters to SQL Server, but getting this error:
System.Data.SqlClient.SqlException: 'Must declare the scalar variable "#Title1Item".'
I've included both the C# and SQL Server side code. Can anyone tell me what I am doing wrong?
public DataTable GetInventoryByAssetDescription (string Desc, string DispositionText, bool Title1Item) {
DataTable myRecords = new DataTable();
using (SqlConnection cn = new SqlConnection (ConnectionString)) {
// Specify which stored procedure to use and add a parameter.
SqlCommand cmd = new SqlCommand ("GetInventoryByAssetDescription", cn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue ("#SearchString", Desc);
cmd.Parameters.AddWithValue ("#DispositionText", DispositionText);
cmd.Parameters.AddWithValue ("#Title1Item", Title1Item);
SqlDataAdapter da = new SqlDataAdapter (cmd);
cn.Open();
da.Fill (myRecords);
}
ALTER PROCEDURE [dbo].[GetInventoryByAssetDescription] (
#SearchString varchar(30),
#DispositionText varchar(200),
#Title1Item bit
)
I expect the 3 parameters to be passed through to the stored procedure, but am getting this error on the da.fill (myRecords) line:
System.Data.SqlClient.SqlException: 'Must declare the scalar variable "#Title1Item".'
EDIT: So sorry, guys. Rusty old programmer here. Stored procedure code:
USE [Inventory]
GO
/****** Object: StoredProcedure [dbo].[GetInventoryByAssetDescription] Script Date: 6/4/2019 8:30:15 AM ******/
SET ANSI_NULLS OFF
GO
SET QUOTED_IDENTIFIER OFF
GO
ALTER PROCEDURE [dbo].[GetInventoryByAssetDescription]
(
#SearchString varchar(30),
#DispositionText varchar(200),
#Title1Item bit
)
AS
begin
SET NOCOUNT ON
declare #sql nvarchar (2000)
select #SearchString=UPPER(#SearchString)
set #sql = ' select in_eq_ID,
in_eq_TagNumber as TagNumber,
Title1Item,
in_eq_AssetDescription as Description,
in_eq_ExtendedDescription as ExtendedDescription,
in_eq_SerialNumber as SerialNumber,
in_eq_ValuationAmount as TotalValue,
in_eq_CustodianName as Name,
in_eq_ComplexBuilding as ShortLocation,
in_eq_SubLocationCode as ShortRoomNumber,
in_ca_Categories.in_ca_CategoryName as CategoryName,
in_eq_DispositionDate as DispositionDate,
DATEADD (dd, 0, DATEDIFF (dd, 0, in_eq_Equipment.in_eq_AcquisitionDate)) as AcquisitionDate
from in_eq_Equipment
LEFT JOIN in_ca_Categories ON in_eq_Equipment.in_eq_CategoryID_fk = in_ca_Categories.in_ca_CategoryID
where #Title1Item = Title1Item
AND upper (in_eq_AssetDescription) LIKE upper ('''+ #SearchString + ''')'
set #sql=#sql+' ' + ISNULL(#DispositionText,' ') + ' order by in_eq_AssetDescription'
execute (#sql)
return
end
When you use the datatype BIT in SQLServer as SP parameter, from C# you have to send the values 1 or 0 not true or false so you have to convert bool to int (1 for true and 0 for false)
For example
cmd.Parameters.AddWithValue ("#Title1Item", Title1Item ? 1 : 0);
or
cmd.Parameters.AddWithValue ("#Title1Item", (int)Title1Item);
Right; the problem is your SP, not the C# code. It cannot work in the way written. Parameters and local variables are not "ambient" - they won't be defined inside an EXEC or EXECUTE block, because they are not scoped in there. In some cases there is sp_executesql that allows you to pass through parameters/values by declaring them and adding them as additional parameters, but in your case this would not be a good idea, because fundamentally you are doing something incredibly dangerous, i.e. concatenating inputs to create SQL. Your stored procedure right now is a SQL injection hole and could be used to take over your server with minimal effort. If you're OK with having your data stolen and your database destroyed, you could inline/concatenate #Title1Item just like you did with the others, but: this is a terrible idea and it will hurt you.
Thanks in advance;
I inherited a stored procedure that increments a record and returns its value in a single call. The intent is to only return a single value just like an Identity() column.
Here is the stored proc:
ALTER PROCEDURE [dbo].[sp_GetNextKey]
#RetVal int OUTPUT,
#Name varchar(250)
AS
UPDATE Keys
SET Key_Next = Key_Next + 1,
#RetVal = Key_Next + 1
FROM Keys
WHERE Key_Table = #Name
Unfortunately, this is VERY old code and I cannot modify the application to implement Identity(). This has worked in production for years being accessed solely by a ColdFusion application. It is now being called by a C# application and we're getting what appears to be a threading issue. I need to solve this in SQL and not in CF or .NET because this procedure is called in a ton of locations in both applications.
They are getting the same values returned as if they live in independent environments. I should add the fact that it does work as expected most of the time, but not always. My guess is that it's only under severe load when it's called in the exact same millisecond in each application.
I would hate to do some sort of lock because this thing is called thousands of times per hour. I'm afraid we'll end up with deadlock issues.
For clarity, here are the calls in CF and C#:
<CFFUNCTION name="getNextId" returntype="numeric" access="public">
<CFARGUMENT name="keyTableName" type="string" required="yes" >
<CFARGUMENT name="dbSource" type="string" required="yes" >
<cfstoredproc procedure="sp_GetNextKey" datasource="#ARGUMENTS.dbSource#" returnCode="No">
<cfprocparam type="OUT" CFSQLType="CF_SQL_INTEGER" variable="RetVal">
<cfprocparam type="IN" CFSQLType="CF_SQL_VARCHAR" value="#UCase(ARGUMENTS.keyTableName)#" maxlength="250">
</cfstoredproc>
<cfquery name="ab" datasource="#ARGUMENTS.dbSource#">
SET ARITHABORT ON
</cfquery>
<cfreturn RetVal >
</CFFUNCTION>
C#:
SqlCommand cmd = new SqlCommand("sp_GetNextKey", conn);
cmd.CommandType = CommandType.StoredProcedure;
var outParam = new SqlParameter("#RetVal", SqlDbType.Int);
outParam.Direction = ParameterDirection.Output;
outParam.Size = 128;
cmd.Parameters.Add(outParam);
cmd.Parameters.Add("#Name", SqlDbType.VarChar);
cmd.Parameters["#Name"].Value = tableName;
conn.Open();
cmd.ExecuteNonQuery();
//get the return value
retVal = Convert.ToInt32(cmd.Parameters["#RetVal"].Value);
conn.Close();
What is the best approach that would allow this thing to return a unique value for any application calling it?
Based on the comments, we tried this but it did not change the results:
ALTER
PROCEDURE [dbo].[sp_GetNextKey]
#RetVal int OUTPUT,
#Name varchar(250)
AS
UPDATE Keys
WITH (ROWLOCK)
SET #RetVal = Key_Next = Key_Next + 1
FROM Keys
WHERE Key_Table = #Name
Also tried but did not work:
ALTER
PROCEDURE [dbo].[sp_GetNextKey]
#RetVal int OUTPUT,
#Name varchar(250)
AS
BEGIN TRANSACTION
EXEC sp_getapplock #LockMode = 'Shared', #Resource = 'Keys';
UPDATE Keys
WITH (ROWLOCK)
SET #RetVal = Key_Next = Key_Next + 1
FROM Keys
WHERE Key_Table = #Name
EXEC sp_releaseapplock #Resource = 'Keys'
COMMIT TRANSACTION
Try this:
ALTER PROCEDURE [dbo].[sp_GetNextKey]
#RetVal int OUTPUT,
#Name varchar(250)
AS
BEGIN TRAN
SELECT #RetVal = MAX(Key_Next) + 1 FROM Keys WHERE Key_Table = #Name
UPDATE Keys SET Key_Next = #RetVal WHERE Key_Table = #Name
COMMIT
This intentionally split the two operations into separate statements, and wraps them in a transaction such that the correct locks should be used and consistency maintained.
Try it
ALTER PROCEDURE [dbo].[sp_GetNextKey]
#RetVal int OUTPUT,
#Name varchar(250)
AS
UPDATE Keys
SET #RetVal = Key_Next = Key_Next + 1
FROM Keys
WHERE Key_Table = #Name
You need to set the value of the variable at the same time that you update the field.
Coldfusion concurrency bug in function:
If the component holding the ColdFusion getNextId function is cached in memory, then it could be causing your problem because the default scope for non-explicitly scoped variables, such as your RetVal variable, is at the component level, not the function as one unfamiliar with CF might assume, making it global to the component. Add the following line just after the arguments in the function to make it no longer susceptible to concurrency issues (defines the variable to be local to the function.)
<cfset var RetVal />
Or, you can explicitly scope the variable to the function by prefixing it with local. everyhwere it appears in the function. Ex: local.RetVal. (works in CF9+)
Here is my stored procedure in SQL Server 2014:
CREATE PROCEDURE [dbo].[spSelectUserFromProfileUsers]
#TableName NVARCHAR(20),
#User NVARCHAR(20)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #query NVARCHAR(MAX);
SET #query = 'SELECT * FROM ' + QUOTENAME(#TableName) + ' WHERE Users =' + #User
EXECUTE sp_executesql #query
END
and here is my code in Visual Studio:
if (TableFunctions.doesTableExist(ComboBoxSelectedProfile.SelectedItem + "Users", dbConnectionString))
{
// DynamicSQL
using (SqlCommand command = new SqlCommand("spSelectUserFromProfileUsers", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.AddWithValue("#TableName", ComboBoxSelectedProfile.SelectedItem + "Users");
command.Parameters.AddWithValue("#User", TextBoxUserName.Text);
command.ExecuteNonQuery();
}
}
and I'm getting the error:
Invalid column name /Text that I entered the textbox/
I've been looking for a solution a long time and I can't find anything, I'll appreciate your help very much!
You need to make sure that your Users value is inside quotes. As it's a sql and there are only single quotes available, you have to do it in that strange way.
SET #query='SELECT * from '+QUOTENAME(#TableName)+' where Users='''+#User+''''
Your stored procedure that tries to execute a dynamic sql has a wrong syntax.
DECLARE #ParmDefinition NVARCHAR(2000);
SET #query='SELECT * from '+QUOTENAME(#TableName)+' where Users=#User';
SET #ParmDefinition = N'#User NVARCHAR(20)';
EXECUTE sp_executesql #query, #ParmDefinition, #User = #User;
From the documentation about sp_executesql you could see that a parameter should be used as is inside the dynamic sql text not trying to extract is value and appending the value. After that you should define the parameter for the sp_executesql (#ParmDefinition) and set its value (#User = #User) as second and third parameter to sp_executesql
query is not properly enclosed.
SET #query="SELECT * from '+QUOTENAME(#TableName)+' where Users='+#User+'"
I'm trying to create a new stored procedure programatically using the following code:
using (MySqlConnection conn = new MySqlConnection(connectionString))
{
conn.Open();
using (MySqlTransaction trans = conn.BeginTransaction())
{
using (MySqlCommand command = conn.CreateCommand())
{
command.CommandText = query;
command.ExecuteNonQuery();
}
trans.Commit();
}
}
And the following text as the create statement copied from Mysql workbench:
static string query = #"
delimiter $$
CREATE PROCEDURE `GetParentIds`(IN `tempTableName` VARCHAR(255), IN `id` int)
BEGIN
DECLARE parId INT;
DECLARE curId INT;
DROP TEMPORARY TABLE IF EXISTS tempTableName;
CREATE TEMPORARY TABLE tempTableName (
node_id INT NOT NULL PRIMARY KEY
);
set curId := id;
get_parents_loop: LOOP
set parId := null;
set parId = (select ParentID from {TableName} where ID = curId);
IF parId is NULL THEN
LEAVE get_parents_loop;
END IF;
INSERT INTO tempTableName(node_id) Values (parId);
set curId := parId;
END LOOP get_parents_loop;
SELECT *
FROM tempTableName;
END$$";
This procedure is passed an ID of an object which has a parentID and it gets all of the parentIDs of all the parents of the given object and returns them. The problem comes when I try to run it and I get the following message:
"You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'delimiter $$
CREATE PROCEDURE GetParentIds(IN tempTableName VARCHAR(255),' at line 1"
Any and all Ideas are welcome!
* EDIT **
Thanks to all the answers below, this is what finally worked:
CREATE PROCEDURE GetParentIds(IN tempTableName VARCHAR(255), IN id int)
BEGIN
DECLARE parId INT;
DECLARE curId INT;
DROP TEMPORARY TABLE IF EXISTS tempTableName;
CREATE TEMPORARY TABLE tempTableName (node_id INT NOT NULL PRIMARY KEY );
set curId := id;
get_parents_loop: LOOP
set parId := null;
set parId = (select ParentID from TDOs where TDOID = curId);
IF parId is NULL THEN
LEAVE get_parents_loop;
END IF;
INSERT INTO tempTableName(node_id) Values (parId);
set curId := parId;
END LOOP get_parents_loop;
SELECT *
FROM tempTableName;
END;
Remove DELIMITER $$ at the beginning and $$ after last END
DELIMITER is a mysql client command that enables you to change its statement terminator temporarily while you define a stored routine.
If you are defining a stored routine from within a programming interface that does not use the semicolon as a statement terminator, semicolons within stored routine definitions do not present any special issues.
I am trying to return scalar from a database like this:
DbConnection cn = GetConnection2();
cn.Open();
// stored procedure
DbCommand cmd = GetStoredProcCommand(cn, "GetReason");
DbParameter param;
param = CreateInParameter("Reason_Number", DbType.String);
param.Value = number;
cmd.Parameters.Add(param);
param = CreateOutParameter("Result", DbType.String);
param.Direction = ParameterDirection.Output;
cmd.Parameters.Add(param);
cmd.ExecuteScalar();
string reason;
reason = cmd.Parameters["#Result"].Value.ToString();
if (cn.State == ConnectionState.Open)
cn.Close();
return reason;
Here is my stored procedure:
-- =============================================
-- Create date: Today
-- Description: Input Reason # and Return Full Reason Name
-- =============================================
ALTER PROCEDURE [dbo].[GetReason]
#Reason_Number nvarchar(50),
#Result nvarchar(50) output
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SELECT #Result = Field1
FROM dbo.Reasons
WHERE Field1 LIKE #Reason_Number + '%';
END
I am getting an error on the ExecuteScalar line:
System.InvalidOperationException occurred
Message="String[1]: the Size property has an invalid size of 0."
What am I doing wrong?
If you want to use ExecuteScalar, your stored proc needs to return the single row, single column from a SELECT:
ALTER PROCEDURE [dbo].[GetReason]
#Reason_Number nvarchar(50)
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
SELECT Field1
FROM dbo.Reasons
WHERE Field1 LIKE #Reason_Number + '%';
END
and then your code needs to read that value:
var returnedValue = cmd.ExecuteScalar();
and use it from there. Of course, in that case, you also do not need an OUTPUT parameter in your C# code....
Word of warning: that SELECT in your stored proc could potentially return multiple rows. You might want to add a TOP 1 to your select - just to be safe:
SELECT TOP 1 Field1
FROM dbo.Reasons
WHERE Field1 LIKE #Reason_Number + '%';
Just add an other one statement to the end of your stored procedure and remove OUTPUT parameter
SELECT #Result as 'Result'