I am trying to get some value in output parameters using SqlCommand Parameters.
The stored procedure runs fine, giving proper records. I verified it executing procedure on Sql Server as well to be sure I am doing it right.
Still, I'm not able to get output on server-side, it always comes nothing! While debugging, I see that IsDirty proeprty for command.Parameters is set to True.
Can anyone tell what does it indicate?
Here is some code :
command.Parameters.Add("#count", SqlDbType.Int).Value = initialCountValue
command.Parameters("#count").Direction = ParameterDirection.Output
Dim dr = command.ExecuteReader()
newCountValue = Convert.ToInt32(command.Parameters("#count").Value)
Here is procedure example:
CREATE PROCEDURE [dbo].[usp_some_procedure]
#filter INT
#count INT output
AS
BEGIN
DECLARE #filtered_ids TABLE(row_num INT IDENTITY(1,1), id INT primary key)
INSERT INTO #filtered_ids ( id )
SELECT id
FROM dbo.some_table
WHERE #filter = 0 OR table_field = #filter
ORDER BY id desc
SELECT #count = COUNT(*)
FROM #filtered_ids
SELECT some_table.*
FROM some_table
INNER JOIN #filtered_ids
ON some_table.id = #filtered_ids.id
END
Output parameters are not visible to the calling code while the reader is opened.
Close the reader that you opened with ExecuteReader, then read your value:
Using dr = command.ExecuteReader()
...
End Using
newCountValue = CInt(command.Parameters("#count").Value)
SqlCommand.Parameters is an instance of SqlParameterCollection. SqlParameterCollection does not have a publicly available member called IsDirty. If you are seeing one in your command.Parameters then it is not an instance of SqlParameterCollection which means that command isn't an instance of SqlCommand.
I did find what appears to be a decompiled version of the SqlParameterCollection class that seems to have an internal member called IsDirty, but you should not have access to this.
It seems to me that you are doing something weird here and it's entirely possible that is the cause of your problem. I would add the default System.Data.SqlClient namespace and see if that resolves your problem.
It might help to post more of your code including where you are getting your SqlCommand class from.
Related
I have a query that I captured through an extended events session with a float datatype that looks like :
#variable = 120700.8000000000000000000000000000000000000000000000
If I try to run the same query in SSMS, I get the error:
The number '120700.8000000000000000000000000000000000000000000000' is out of the range for numeric representation (maximum precision 38).
Same as if you were to run
DECLARE #variable float = 120700.8000000000000000000000000000000000000000000000
The query succeeded when the trace was running. The Event is rpc_completed and it has a duration, cpu, rowcount, etc...
Not sure if this is relevant, but the query in question is an exec sp_executeSQL. The full query as captured by the trace looks like:
exec sp_executeSQL N'SELECT
col1,
col2
FROM table
WHERE col3 > #variable', N'#variable float',
#variable = 120700.8000000000000000000000000000000000000000000000
So my question is, why is the code in the trace able to execute without error, but when I copy/paste that same code to SSMS it throws an error. If I take that same code and trim off a bunch of those zeros it works.
I am running on SQL Azure DB. I have reproduced on compatibility SQL2016, SQL2019, and on Prem SQL2019.
why is the code in the trace able to execute without error, but when I
copy/paste that same code to SSMS it throws an error.
When you put this query in SSMS
exec sp_executeSQL N'SELECT
col1,
col2
FROM table
WHERE col3 > #variable', N'#variable float',
#variable = 120700.8000000000000000000000000000000000000000000000
the literal 120700.8000000000000000000000000000000000000000000000 is interpreted as a numeric/decimal type. The engine tries to convert it to a value of numeric/decimal type and fails, because numeric has maximum precision of 38 digits.
If you put this query in SSMS, it should work
exec sp_executeSQL N'SELECT
col1,
col2
FROM table
WHERE col3 > #variable', N'#variable float',
#variable = 120700.8000000000000000000000000000000000000000000000E0
I've added E0 at the end.
To make the float literal we need to use the scientific notation, as documented in Constants (Transact-SQL)
why is the code in the trace able to execute without error
I can't say for sure, but I suspect that the code that you see in the trace is not exactly the same code that was sent to the SQL Server by the application. It could have been sent in such a way that the parameter type and value was actually float, not a text string with all these zeroes.
I think, the application is likely to make an RPC call passing proper parameter with proper type to the function call. So, there was never a conversion from a text string to numeric or float type when an application made this successful call.
The string literal 120700.8000000000000000000000000000000000000000000000 is first implicitly converted to decimal, hence the error.
Try
DECLARE #variable float = 120700.8000000000000000000000000000000000000000000000E0
Source: TSQL - make a literal float value
Details on what I did to proof out the accepted answer above.
I was taking the trace statement and replaying it on another DB using C# like this (this will throw the out of range error):
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = "exec sp_executesql #stmt=N'SELECT #f',
#params=N'#f float',#f=120700.800000000002910383045673370361328125";
cmd.ExecuteNonQuery();
What I need to do is extract those parts and build a stored procedure with parameters like this :
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.CommandText = "sp_executesql";
SqlParameter p1 = new SqlParameter();
p1.DbType = System.Data.DbType.String;
p1.ParameterName = "#stmt";
p1.Value = "SELECT #f";
SqlParameter p2 = new SqlParameter();
p2.DbType = System.Data.DbType.String;
p2.ParameterName = "#params";
p2.Value = "#f float";
SqlParameter p3 = new SqlParameter();
p3.DbType = System.Data.DbType.Double;
p3.Value = 120700.8000000000000000000000000000000000000000000000;
p3.ParameterName = "#f";
cmd.Parameters.Add(p1);
cmd.Parameters.Add(p2);
cmd.Parameters.Add(p3);
cmd.ExecuteNonQuery();
When I run that, it shows up in the trace as:
exec sp_executesql #stmt=N'SELECT #f',#params=N'#f float',#f=120700.800000000002910383045673370361328125
So the issue is that I cannot just take the statement from an rpc_completed event. I need to parse it and reconstruct the stored procedure explicitly.
I am not sure if this is possible but I have not been able to come across clear documentation for this use case. I am using F# 4 and the FSharp.Data.SqlClient library to connect to SQL Server 2016. I am wanting to call a stored procedure that returns multiple tables and turn those tables into the corresponding records. In this case the first table is made up of items and the second table is made up of customers.
My instinct is that it should look something like this:
let items, customers = cmd.Execute()
My gut is that items would be an IEnumerable<item> and customers would be an IEnumerable<customer> where item and customer are both Record types. What it appears is happening though is that FSharp.Data.SqlClient is only seeing the first returned table from the stored procedure. I am working on a SQL Server 2016 Developer instance. Here is the T-SQL to setup the example:
create table Item (
ItemID int identity(1, 1) primary key,
ItemName nvarchar(50)
)
go
create table Customer (
CustomerID int identity(1, 1) primary key,
CustomerName nvarchar(50)
)
go
insert into Item (ItemName) values ('A');
insert into Item (ItemName) values ('B');
insert into Item (ItemName) values ('C');
insert into Customer (CustomerName) values ('Gary');
insert into Customer (CustomerName) values ('Sergei');
insert into Customer (CustomerName) values ('Elise');
go
create procedure dbo.ExampleProcedure
as
begin
set nocount on;
select
ItemID,
ItemName
from Item
select
CustomerID,
CustomerName
from Customer
end;
And here is the F# script that I am testing with. It shows what I would like to be able to do but I get a compile error on the last line:
#r "../packages/FSharp.Data.SqlClient.1.8.2/lib/net40/FSharp.Data.SqlClient.dll"
#r "../packages/FSharp.Data.2.3.2/lib/net40/FSharp.Data.dll"
#r "System.Xml.Linq.dll"
open FSharp.Data
[<Literal>]
let connStr =
"Data Source=**connection string**;"
type queryExample = SqlProgrammabilityProvider<connStr>
do
use cmd = new queryExample.dbo.ExampleProcedure(connStr)
let items, customers = cmd.Execute()
I am wanting items to correspond to the first returned table and customers to correspond to the second returned table. The intellisense suggests that FSharp.Data.SqlClient is only seeing the first table. When I hover over cmd.Execute() the popup says "This expression was expected to have type 'a*'b but here has type System.Collections.Generic.IEnumerable<SqlProgrammabilityProvider<...>.dbo.ExampleProcedure.Record>". If I do the following I get access to the Items query in the stored procedure:
// Learn more about F# at http://fsharp.org. See the 'F# Tutorial' project
// for more guidance on F# programming.
#r "../packages/FSharp.Data.SqlClient.1.8.2/lib/net40/FSharp.Data.SqlClient.dll"
#r "../packages/FSharp.Data.2.3.2/lib/net40/FSharp.Data.dll"
#r "System.Xml.Linq.dll"
open FSharp.Data
[<Literal>]
let connStr =
"Data Source=**connection string**;"
type queryExample = SqlProgrammabilityProvider<connStr>
do
use cmd = new queryExample.dbo.ExampleProcedure(connStr)
for item in cmd.Execute() do
printfn "%A" item.ItemID
Is this even possible? Is my approach wrong? I could not find clear documentation on this use case but I thought it would be common enough it would be covered.
Update
Just to clarify what I am trying to achieve I am showing how I solve this in C#. In C# I create a DataSet object and populate it with the results of the Stored Procedure. From there I pick out the individual tables to work with. After extracting the tables I then use LINQ to transform the rows into the corresponding objects. It often looks something like the following:
using System.Data;
using System.Data.SqlClient;
var connStr = "**connection string**"
var sqlConnection = new SqlConnection(connStr );
var sqlCommand = new SqlCommand("ExampleProcedure", sqlConnection);
sqlCommand.CommandType = CommandType.StoredProcedure;
var dataSet = new DataSet();
var adapter = new SqlDataAdapter(sqlCommand);
adapter.Fill(dataSet);
var itemsTable = dataSet.Tables[0];
// Turn the itemsTable into a List<Item> using LINQ here
var customersTable = dataSet.Tables[1];
// Turn the customersTable into List<Customer> using LINQ here
I find this to be overly verbose for such a simple thing as extracting the individual tables but perhaps I am too sensitive to code clutter. I know that F# must have a more elegant and terse way to express this.
I don't know F#, however this is a data access problem.
When a stored procedure returns multiple resultsets, you need to access they in sequence, one by one.
cmd.ExecuteReader() returns an instance of a datareader pointing to the first resultset. You need to process this resultset, may be filling a list with instances of a custom class, than you call the method "NextResult" and you will have access to the next resultset and so on.
A reference for the method "NextResult": https://msdn.microsoft.com/pt-br/library/system.data.sqlclient.sqldatareader.nextresult(v=vs.110).aspx
I have couple insert queries which are merged in transaction. First of that insert is to create new product articel number incrementing the most higher in table by one. Unfortunetly i just noticed that mostly during tests if for instance two users from two diffrent applications click button which trigger my transaction's method they could get same new product number. How can avoid that situation? Is there something like lock on first insertion so that if first user accessing table to insert restrict other's user/s about their insertion so they have to wait in queue after first user insert is finished? Is there something like that? Besides i thought if someone inserts other users are not able to insert. I made comments in code you to understand.
Part of my transaction query below:
Public Sub ProcessArticle(ByRef artikel As ArticlesVariations)
Dim strcon = New AppSettingsReader().GetValue("ConnectionString", GetType(System.String)).ToString()
Using connection As New SqlConnection(strcon)
connection.Open()
Using transaction = connection.BeginTransaction()
Try
For Each kvp As KeyValuePair(Of Integer, Artikel) In artikel.collection
articleIndex = kvp.Key
Dim art As Artikel = kvp.Value
Using cmd As New SqlCommand("INSERT INTO tbArtikel (Nummer) VALUES (#Nummer);Select Scope_Identity()", transaction.Connection)
cmd.CommandType = CommandType.Text
cmd.Connection = connection
cmd.Transaction = transaction
'Get next product number from table tbArtikel (this will be new product number)'
Dim NewArtNummer as String = New DALArtikel().GetNewArtikelNumber(transaction)
art.Nummer = NewArtNummer
cmd.Parameters.AddWithValue("#Nummer", art.Nummer)
'Get inserted product id for other diffrent inserts below'
newArticleRowId = CInt(cmd.ExecuteScalar())
'....
other INSERTs queries to other tables ...
...'
transaction.Commit()
Catch ex As Exception
transaction.Rollback()
Throw 'Rethrow exception.'
End Try
End Using
End Using
End Sub
Just about the only way to assure that users are not assigned the same values is to issue them from the server when the row is inserted. It is the entire premise behind the server issuing AI values for PKs.
BUT since your thing is a multi-segment, "numeric string" that presents a problem. Rather than tearing the string apart to find the Max()+1 for one segment with a WHERE clause on parts of the string. Consider something like this:
Start with a table used to increment and issue the values:
{DocId Int, SegmentB int, SegmentC Int}
This will simply track the values to use in the other table. Then a stored procedure to create/increment a new code (MySQL - this is a conceptual answer):
CREATE DEFINER=`root`#`localhost` PROCEDURE `GetNextProductCode`(in docId int,
in Minr int,
in Rev int
)
BEGIN
SET #maxR = 0;
SET #retCode ='';
if Minr =-1 then
Start transaction;
SET #maxR = (SELECT Max(SegmentB) FROM articlecode WHERE MainId = docId) + 1;
UPDATE articlecode SET SegmentB = #maxR WHERE MainId = docId;
Commit;
Select concat(Cast(docId As char) , '.',
Cast(#maxR AS char) , '.',
Cast(Rev As char)
);
end if;
END
This is a rough idea of the process. As such, it only works on the second segment (I dunno what happens when you create a NEW SegmentB - does SegmentC reset to 1???). The idea is:
pass numbers so there is no need to tear up a string
pass -1 for the segment you need the next value for
the sp gets the Max()+1 and updates the counter table so the next user will get a new value
If for some reason you end up not saving the row, there will be gaps
the sp uses a transaction (probably only needs to protect the update) so that only 1 update can happen at a time
returns the new code. it could just return 2 values, but your going to glue them together anyway
There is much To Do:
It only does SegmentB
For a NEW DocId (-1), insert a new row with 1000 and 1(?) defaults
Same for a NEW segmentB (whatever it is): insert a new row for that DocId with default values
To get a new code before you insert a row:
cmd.CommandType = CommandType.StoredProcedure
cmd.Parameters.Add("docId", MySqlDbType.Int32).Value = 3
cmd.Parameters.Add("Minr", MySqlDbType.Int32).Value = -1
cmd.Parameters.Add("Rev", MySqlDbType.Int32).Value = 1
dbcon.Open()
Using rdr = cmd.ExecuteReader()
rdr.Read()
Console.WriteLine(rdr(0))
End Using
The obvious downside is that each insert requires you to hit the DB in order to...well save to the DB. If they were int values it could be a Trigger.
I'm a SQL developer and my VB skills are about fifteen years out of date, but instead of creating the incremented number yourself in VB just let SQL generate them with an IDENTITY field. SQL will never allow duplicates and then you just need to return the SCOPE_IDENTITY():
ALTER TABLE dbo.tbArtikel
ADD [ArtikelID] INT IDENTITY(1,1) PRIMARY KEY;
I have two suggestions:
First suggestion: move your code to a stored procedure this way all your users will execute the same transaction where you can set your isolation level the way you want. Read This.
Second suggestion: I would create a unique index on your field Nummer. This way when I try to insert a duplicate value it will raise an error that I can deal with it by telling the user that he need to retry the same operation or retry it automatically.
Trying to lock the record or the table for your operation is not advisable, however you can check this article on code project you might find what you are looking for. Make sure that you provide a mechanism of releasing all locks if your program stops at the middle of the transaction.
I have a stored procedure in a Microsoft SQL Server database:
USE [ProjectIndexer]
GO
/****** Object: StoredProcedure [files].[add_file] Script Date: 12/12/2014 1:34:20 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [files].[add_file]
#FILENAME varchar(255),
#EXTENSION nvarchar(8),
#PATH_FULL varchar(255),
#RELATIVE_PATH varchar(255),
#JOB varchar(15),
#DATE_MODIFIED datetimeoffset(7),
#SIZE BIGINT,
#INDEX_MODULE INT,
#FILE_TYPE varchar(255),
#DOC_TYPE varchar(255),
#DISCIPLINE varchar(255)
AS
DECLARE #file_id sql_variant
--Insert values for new file
INSERT INTO files.GENERAL
(FILENAME, EXTENSION, PATH_FULL, RELATIVE_PATH, JOB, DATE_MODIFIED, SIZE, INDEX_MODULE, FILE_TYPE, DOC_TYPE, DISCIPLINE)
VALUES(#FILENAME, #EXTENSION, #PATH_FULL, #RELATIVE_PATH, #JOB, #DATE_MODIFIED, #SIZE, #INDEX_MODULE, #FILE_TYPE, #DOC_TYPE, #DISCIPLINE);
--Get the ID of this new file
SELECT #file_id = current_value FROM sys.sequences WHERE name = 'FILE_ID_SEQ';
--Return ID
RETURN CONVERT(bigint, #file_id)
I am trying to run this procedure in a VB application developed in Visual Studio 2012, using table adapters:
Dim myFilesGeneralTableAdapter As New FILES_GeneralTableAdapter
Dim FileID As Int32
FileID = myFilesGeneralTableAdapter.AddFile(FileName, fileExtension, foundFile, fileRelativePath, JobNumber, fileDateModified, fileSize, Nothing, Nothing, Nothing, Nothing)
For some reason, the function isn't returning the value to the VB variable 'FileID'. I can, however, use the "Preview Data" feature in the dataset designer to insert values for the above parameters in Visual Studio, and in that environment I'm able to get a returned value.
This suggests to me that my syntax in my VB module is wrong. Can anyone tell me what the error is?
Have you considered using System.Data.SqlClient.SqlCommand? You can use it to call your stored procedure, add the parameter values, and execute it in such a way that you retrieve a single value, i.e.
Try
'Create command'
Dim command As New SqlCommand
command.CommandType = CommandType.StoredProcedure
conn = New SqlConnection(myConnString)
command.Connection = conn
command.CommandText = "add_file"
command.Parameters.AddWithValue("#Filename", [Filename])
...
command.Parameters.AddWithValue("#Discipline", Discipline)
command.Connection.Open()
Dim i As Int64 = command.ExecuteScalar()
command.Connection.Close()
'returns record ID'
Return i
Catch ex As Exception
Throw ex
Finally
If conn.State <> ConnectionState.Closed Then
conn.Close()
End If
End Try
Please note that, in this example, "myConnString" is a String with the value of my connection string for the database I'm connecting to.
I took the good advice of transitioning the script to an Entity Framework model. At first, the stored procedure did not return what I wanted (it only returned an integer value of 1), but I found a resource online that instructed me to create a "Function Import" in the EF model. After doing that and properly matching variable types I managed to get the stored procedure to return the right value in Visual Studio. Here's a good resoure:
http://www.devtoolshed.com/using-stored-procedures-entity-framework-scalar-return-values
Outside of converting from TableAdapter to Entity Framework (a good move regardless), you would be much_better off returning such a value as either an OUTPUT parameter or in a result set. A stored procedure return value is really intended to convey a status code of the operation and is hence pretty limited.
Also, by doing the following operation after the INSERT (i.e. as a separate step),
--Get the ID of this new file
SELECT #file_id = current_value FROM sys.sequences WHERE name = 'FILE_ID_SEQ';
you are risking getting an incorrect value if some other process inserts into that table at roughly the same time (i.e. between this particular INSERT and this particular SELECT).
Given both of the above issues, it would be fairly simple, and very reliable, to instead use the OUTPUT clause on the INSERT statement:
--Insert values for new file
INSERT INTO files.GENERAL
(FILENAME, EXTENSION, PATH_FULL, RELATIVE_PATH, JOB, DATE_MODIFIED, SIZE,
INDEX_MODULE, FILE_TYPE, DOC_TYPE, DISCIPLINE)
OUTPUT INSERTED.FILE_ID
VALUES(#FILENAME, #EXTENSION, #PATH_FULL, #RELATIVE_PATH, #JOB, #DATE_MODIFIED,
#SIZE, #INDEX_MODULE, #FILE_TYPE, #DOC_TYPE, #DISCIPLINE);
That will generate a 1 row, 1 column result set. The column will be named FILE_ID or whatever the actual field name is (I guess based on the naming convention used for the other fields and the variable).
Then get rid of the DECLARE #file_id, the SELECT ..., and the RETURN ....
I have a stored procedure in MS SQL 2008, which compiles without error, but when go to execute the stored procedure I receive the error message 'String or binary data would be truncated'. Unfortunately, I am unable to trace where this is happening.
When I developed my Stored Procedure, I tested the query first, and it worked.
So, if anyone can show where and why this happening to me and how to solve it, I would be grateful:
ALTER PROCEDURE [dbo].[sp_Material_Validation]
#ValidType varchar(MAX),
#MaterialID varchar(MAX)
AS
BEGIN
DECLARE
#MessageReturn varchar(100);
SET #MessageReturn = NULL;
IF #ValidType = 'Primary'
Select TOP 1 Valid.StatusMessage
From
(Select MatID,'Property Not Completed' as StatusMessage
From dbo.Properties
WHERE DATALENGTH(ValueString)=0
UNION
Select PrpCount.MatID,'There not all Properties exist' as StatusMessage
From (Select Count(unkey) as unKeyCt, MatID
From Properties
Group By MatID
Having NOT Count(Cast(UnKey as Int)) = 19)AS PrpCount
UNION
SELECT Header,'Material does not exist' as StatusMessage
FROM Materials Item
Where NOT EXISTS (Select ID
FROM MatMaster Mat
WHERE MAT.ID= Item.HEAD)
UNION
SELECT Header,'Material(s) not Complete' as StatusMessage
FROM Materials
Where MatID IN (Select ID
FROM MatMaster
WHERE INC ='Y')) as Valid
Where Valid.MatID = #MaterialID;
IF #MessageReturn IS NOT NULL
UPDATE dbo.Mat_DEV
SET Inco ='Y',
User_Message = #MessageReturn
WHERE MatID = #MaterialID
ELSE
UPDATE dbo.Mat_DEV
SET User_Message = 'No Validation Errors'
WHERE MatID = #MaterialID
String or binary data would be truncated only appears when you try and put a value in a box that isn't big enough to hold the value.
Now, you're only actually writing values in two places, so the candidates for your problem are:-
UPDATE dbo.Mat_DEV
SET Inco ='Y',
User_Message = #MessageReturn
WHERE MatID = #MaterialID
..or..
UPDATE dbo.Mat_DEV
SET User_Message = 'No Validation Errors'
WHERE MatID = #MaterialID
Check the column definitions for both these columns ( Inco and User_Message ). My guess is that one is too small to hold the value you want to set.
My guess is that column User_Message does not have enough length
I had the same problem.
When I executed the stored proc directly, everything worked as it should.
When I called the same proc from ASP.NET, I got 'String or binary data would be truncated'...
Finally I realized the culprit was a varchar(20) field with the default value 'suser_sname()'... in SQL Management Studio the user name was short, from ASP.NET it was more than 20 chars... A simple CAST solved the problem. :)