Can't read data from varchar(max) using ADODB - sql-server

I've got a method that uses ADODB to execute a sproc and read the results as a recordset. Everything was working fine until i changed one of the output fields from varchar(2000) to varchar(max) (SQL2008). Now I can't read the data from that field. The strange thing is that the data is visible in the debugger immediately after running the Execute, but stepping in the debugger makes the data vanish.
Here is the code:
Public Function sproc_RptEAComment(ByVal accountName As String, ByVal contractName As String,
ByVal acctType As String, ByVal asOfDate As DateTime,
ByVal sessionID As String, ByVal clin As String,
ByVal dollarDisplay As String) As List(Of sproc_RptEAComment_Row) Implements ISprocRepository.sproc_RptEAComment
Try
Dim cmd = New Command()
cmd.ActiveConnection = Connection
cmd.CommandType = CommandTypeEnum.adCmdStoredProc
cmd.CommandText = "sproc_RptEAComment"
ADOUtilities.AddParamToSproc(cmd, "#ChargeNum", accountName)
ADOUtilities.AddParamToSproc(cmd, "#Contract", contractName)
ADOUtilities.AddParamToSproc(cmd, "#EmployeeName", "")
ADOUtilities.AddParamToSproc(cmd, "#Org", acctType)
ADOUtilities.AddParamToSproc(cmd, "#HoursVal", "TRUE")
ADOUtilities.AddParamToSproc(cmd, "#Sort", "1")
ADOUtilities.AddParamToSproc(cmd, "#Employer", "")
ADOUtilities.AddParamToSproc(cmd, "#Type", "1")
ADOUtilities.AddParamToSproc(cmd, "#FromD", asOfDate.ToShortDateString())
ADOUtilities.AddParamToSproc(cmd, "#ToD", asOfDate.AddMonths(-5).ToShortDateString())
ADOUtilities.AddParamToSproc(cmd, "#SessionID", sessionID)
ADOUtilities.AddParamToSproc(cmd, "#Clin", clin)
ADOUtilities.AddParamToSproc(cmd, "#RptDisplay", "")
ADOUtilities.AddParamToSproc(cmd, "#Parm_DT", dollarDisplay)
Dim lst = New List(Of sproc_RptEAComment_Row)
Dim rs = cmd.Execute()
While Not (rs.EOF)
Dim newEntity = New sproc_RptEAComment_Row(rs)
lst.Add(newEntity)
rs.MoveNext()
End While
Return lst
Catch ex As Exception
MsgLogger.Err(ex)
Return Nothing
End Try
End Function
If I look in the debugger immediately after the Execute I see this. Note the field EacJustCom has the proper string value:
I take one step in the debugger and see this. THe value is gone. Note the field "_Account" is still intact (it's defined as varchar(100)):

The problem you are having is that a varchar(max) can hold up to 2 GB of data.
In the old days with Visual Basic and DAO, there was a method call GetChunk() to read bits of data from a binary field and AppendChunk() to write bits. Use a binary or text file stream to put the data back together on the client side.
"Use the GetChunk method on a Field object to retrieve part or all of its long binary or character data."
See the MSDN reference.
These feilds have to be handled differently than a regular record set. Exclude them from your record set.
Check out this article Who is afraid of the big bad blob?
Since there are so many development environments, vs xxx or ADO or ADO.NET, it is difficult to point out your particular issues w/o more information.
Write back to me if this helps ...

I never found the answer to the problem, but did find a workaround. If I create the recordset first and fill it from the command, it works.
Dim rs As New ADODB.Recordset()
rs.CursorLocation = ADODB.CursorLocationEnum.adUseClient
rs.CursorType = ADODB.CursorTypeEnum.adOpenForwardOnly
rs.LockType = ADODB.LockTypeEnum.adLockBatchOptimistic
rs.StayInSync = False
rs.Open(cmd)
rs.ActiveConnection = Nothing
Dim lst = New List(Of sproc_RptEAComment_Row)
While Not (rs.EOF)
Dim newEntity = New sproc_RptEAComment_Row(rs)
lst.Add(newEntity)
rs.MoveNext()
End While
Return lst

Related

String or binary data would be truncated. Is it something wrong with declaring "imagelocation"?

Currently I'm developing a student portal with an appropriate and simple login system for my college's final year project purpose. This is one of the command button (to save photo to SQL server) I've encountered error. This is the error statement:
System.Data.SqlClient.SqlException: 'String or binary data would be truncated.
The statement has been terminated.'
Private Sub Button8_Click(sender As Object, e As EventArgs) Handles Button8.Click
'String imagelocation = ""
Dim images() As Byte = Nothing
'Dim imagelocation As String
'imagelocation = ""
Dim Stream As New FileStream(imagelocation, FileMode.Open, FileAccess.Read)
Dim brs As New BinaryReader(Stream)
images = brs.ReadBytes(CInt(Stream.Length))
Dim source As String = "Data Source=LAPTOP-85ALBAVS\SQLEXPRESS;Initial Catalog=Portal;Integrated Security=True"
Dim con As New SqlConnection(source)
con.Open()
Dim cmd As String = "Insert into Photo Values('" + TextBox2.Text + "', #images)"
Dim qry As New SqlCommand(cmd, con)
qry.Parameters.Add(New SqlParameter("#images", images))
'qry.Parameters.Add(new SqlParameter("#images", pictureBox1));
***Dim i As Integer = qry.ExecuteNonQuery()
If i >= 1 Then
MessageBox.Show("Successfull!", "message", MessageBoxButtons.OK)
Else
MessageBox.Show("Fail!", "message", MessageBoxButtons.OK)
End If
End Sub
End Class
This is a SQL table for me to save the uploaded photo into database. Any help would be appreciated. Thanks.
UPDATE
PreviouslyPhotocolumn name changed toImg, Photowould be the table name.
So I tried to switch the code into another method which is:
Dim source As String = "Data Source=LAPTOP-85ALBAVS\SQLEXPRESS;Initial Catalog=Portal;Integrated Security=True"
Dim con As New SqlConnection(source)
Dim command As New SqlCommand("Insert into Photo (Img, Pname) Values (#Img, #Pname)", con)
Dim ms As New MemoryStream
pictureBox1.Image.Save(ms, pictureBox1.Image.RawFormat)
command.Parameters.Add("#Img", SqlDbType.Image).Value = ms.ToArray()
command.Parameters.Add("#Pname", SqlDbType.VarChar).Value = TextBox2.Text
con.Open()
If command.ExecuteNonQuery() = 1 Then
MessageBox.Show("Successfully uploaded", "Message", MessageBoxButtons.OK)
Else
MessageBox.Show("Failed. Try again.", "Message", MessageBoxButtons.OK)
End If
So it's actually worked I guess. Not sure if there would be any hidden error. Any comment would be helpful guys. This is the output of Photo table.
Phototable output
This error occurs when you specify a the size of a parameter and then provide data that is larger than that. For a start, the way you're adding the parameter is bad:
qry.Parameters.Add(New SqlParameter("#images", images))
You're not specifying a data type or a size there so you're relying on the system default type being OK. Obviously it is not or you would not be getting this error. ALWAYS specify the data type and, if the data type is variable-size, the size as well, e.g.
qry.Parameters.Add("#images", SqlDbType.VarBinary, 8000).Value = images
The SqlDbType value you specify should match the data type of the column the data is for and the size should match the size in the database too. If you use varbinary(max) in the database then use -1 for the parameter size.
First, you should know that IMAGE data type usage is deprecated. Usage of VARBINARY(MAX) is more recommended:
ALTER TABLE TableName ALTER COLUMN Photo VARBINARY(MAX)
The explanation about image data type deprecation can be seen here.
For storing images you have to make use of the varbinary(MAX)
datatype. The image datatype will be deprecated.
Next, the exception occurred because you're adding data to image column which has smaller size than passed image from parameter (because SqlDbType is not specified, CLR infers Byte(n) type automatically; hence data truncation may occur to fit passed array for IMAGE data type). Use SqlDbType.VarBinary with size set to -1:
qry.Parameters.Add("#images", SqlDbType.VarBinary, -1).Value = images
Note that you can set maximum size of VARBINARY with certain numbers in bytes (other than -1), but you need to check against images.Length to prevent truncation (simply cancel upload process if image size is larger than specified).
If images.Length > 1048576 Then ' maximum limit e.g. 1 MiB
' cancel upload
Else
' continue and save to DB
End If
Finally, adjust the query to use parameters for all values:
Dim source As String = "Data Source=LAPTOP-85ALBAVS\SQLEXPRESS;Initial Catalog=Portal;Integrated Security=True"
Using con As New SqlConnection(source)
con.Open()
Dim cmd As String = "Insert into Photo Values(#pname, #images)"
Using qry As New SqlCommand(cmd, con)
qry.Parameters.Add("#pname", SqlDbType.NVarchar, 50).Value = TextBox2.Text
qry.Parameters.Add("#images", SqlDbType.VarBinary, -1).Value = images
Dim i As Integer = qry.ExecuteNonQuery()
' other stuff
End Using
End Using

SQL Parameter value is getting truncated

I have the following code which simply executes a stored procedure which accepts 1 parameter.
Public Function GetData(ByVal Faccode As String, Optional ByRef s As String = "") As DataSet
Dim params As SqlParameter() = {New SqlParameter("#aFacilityCode", SqlDbType.VarChar, ParameterDirection.Input)}
' Set the value
params(0).Value = "SW29" 'Faccode
Try
Dim DSet As DataSet = RunProcedure("usp_FL_GetAllData", params, "ContactData")
Return DSet
Catch ex As Exception
Return Nothing
End Try
End Function
Protected Overloads Function RunProcedure( _
ByVal storedProcName As String, _
ByVal parameters As IDataParameter(), _
ByVal tableName As String) _
As DataSet
Dim dataSet As New dataSet
Try
myConnection.Open()
Dim sqlDA As New SqlDataAdapter
sqlDA.SelectCommand = BuildQueryCommand(storedProcName, parameters)
sqlDA.Fill(dataSet, tableName)
Return dataSet
Catch ex As Exception
Return Nothing
Finally
If myConnection.State = ConnectionState.Open Then
myConnection.Close()
End If
End Try
End Function
Private Function BuildQueryCommand( _
ByVal storedProcName As String, _
ByVal parameters As IDataParameter()) _
As SqlCommand
Dim command As New SqlCommand(storedProcName, myConnection)
command.CommandType = CommandType.StoredProcedure
Dim parameter As SqlParameter
For Each parameter In parameters
command.Parameters.Add(parameter)
Next
Return command
End Function
The SQL procedure is defined like so:
CREATE PROCEDURE [dbo].[usp_FL_GetAllData]
(
#aFacilityCode VARCHAR(10)
)
When I run the software, SQL Profiler shows this call is being made:
exec usp_FL_GetAllData #aFacilityCode='S'
Initially, I was assigning the value Faccode for my parameter in the GetData function but noticed this weird truncation, which is why I'm now hardcoding the value.
The only thing I could think of is that the SQL procedure defined the parameter as a varchar(1) but it's defined as 10 so I don't know why this is happening. RunProcedure is used in many places which do not exhibit this behavior.
What else could be causing this?
To see why removing the parameter direction from your constructor call solves the problem, take a look at the list of constructors defined by the SqlParameter class. Note that there is no constructor that takes a parameter name, SqlDbType, and ParameterDirection; the constructor you're actually invoking is this one, whose third parameter is the parameter size. Because the backing value of ParameterDirection.Input is 1, you are explicitly setting the size of the parameter to one character.
When you instead invoke a constructor that doesn't explicitly give a size, the object infers the size of the parameter from the value that you assign, as described in the documentation for that property.
This makes a number of important changes to your code. It does address the parameter length issue, but you'll need to check if it actually helps.
's was not used, and ByRef is a code smell in .Net
Public Function GetData(ByVal Faccode As String) As DataSet
Dim params As New SqlParameter("#aFacilityCode", SqlDbType.VarChar, 10)
If String.IsNullOrEmpty(Faccode) Then Faccode = "SW29"
params.Value = Faccode
'Removed Try/Catch handler. It's NEVER a good idea to just swallow exceptions like that. Let the exception bubble up to higher level code that knows how to handle it.
Return RunProcedure("usp_FL_GetAllData", "ContactData", params)
End Function
'Note the change to SqlParameter. IDataParameter does not have a Length or Size property. That MIGHT be your problem.
'Also note the use of ParamArray... required changing the order of the arguments, but helped simplify code in the first function
Protected Overloads Function RunProcedure( _
ByVal storedProcName As String, ByVal tableName As String, _
ByVal ParamArray parameters() As SqlParameter) _
As DataSet
Dim dataSet As New dataSet
Using myConnection As New SqlConnection("string here"), _
command As SqlCommand = BuildQueryCommand(storedProcName, parameters), _
sqlDA As New SqlDataAdapter(command)
command.Connection = myConnection
sqlDA.Fill(dataSet, tableName) '.Fill() will open the connection for you if needed
End Using
Return dataSet
End Function
Private Function BuildQueryCommand( _
ByVal storedProcName As String, _
ByVal ParamArray parameters() As SqlParameter) _
As SqlCommand
Dim command As New SqlCommand(storedProcName)
command.CommandType = CommandType.StoredProcedure
If parameters IsNot Nothing Then command.Parameters.AddRange(parameters)
Return command
End Function
Note these changes WILL likely impact other code in your application, but they are important.
Changing the first line of GetData from this:
Dim params As SqlParameter() = {New SqlParameter("#aFacilityCode", SqlDbType.VarChar, ParameterDirection.Input)}
To this:
Dim params As SqlParameter() = {New SqlParameter("#aFacilityCode", SqlDbType.VarChar)}
Fixed my issue. I'm not sure why, and if anyone knows I'd love to know why.

Return result set from SQL Server stored procedure to vb.net

The following stored procedure works as I want in the Visual Studio designer. The result is a table containing all the race distances for the input #CourseName
ALTER PROCEDURE [dbo].[getCourseDistancesProc]
#CourseName nvarchar(50)
AS
BEGIN
SET NOCOUNT ON;
SELECT DISTINCT
RaceDistances.RaceDistance
FROM
RacingMaster
JOIN
RaceDistances ON RacingMaster.Dist_Of_Race_FK = RaceDistances.PKRaceDistancesId
JOIN
Courses ON RacingMaster.RM_Course_FK = Courses.PKCourseId
WHERE
CourseName = #CourseName
END
I want to call the stored procedure from a vb.net application. What data type do I declare as the output variable so that the full result set is returned to the calling app?
There was obviously more work to be done than I had realized, but just in case anyone else stumbles across this question the solution I finally adapted from elsewhere is:-
Dim myConn As SqlConnection
Dim myCmd As SqlCommand
Dim results As String
Dim ConnectionString As String
' Create the connection string.
ConnectionString = "Data Source=*********;" & _
"Initial Catalog=*******;" & _
"Integrated Security=SSPI;"
myConn = New SqlConnection(ConnectionString)
myConn.Open()
Dim InputName As String
InputName = TextBox1.Text
myCmd = New SqlCommand()
myCmd.CommandText = "getCourseDistancesProc"
myCmd.CommandType = CommandType.StoredProcedure
myCmd.Parameters.AddWithValue("#CourseName", Odbc.OdbcType.NVarChar).Value = InputName
myCmd.Connection = myConn
Dim myReader As SqlDataReader = myCmd.ExecuteReader()
If myReader.HasRows Then
Do While myReader.Read()
Dim var As String
var = myReader.GetString(0)
MsgBox(var)
Loop
Else
MsgBox("No rows found.")
End If
myReader.Close()
Obviously, the above is just to demonstrate that the requested data is indeed coming back from the database. But now I know that it is I can handle it in a more useful way.

Retrieving Stored Procedure Results

I'm trying to switch a user between three different screens depending on what a stored procedure returns in a BtnView_Click procedure in asp.net VB. The SP would return a "0, 1, or NULL". Currently it's only returning a "1" and not the others. I'm having trouble with the Reader.Read area with the IF statement and i'm wondering if there's a simple fix to this so it directs everything accuratley.
This is what I currently have (updated)
Sub BtnView_Click(ByVal sender As Object, ByVal e As CommandEventArgs)
Session.Add("SvarRecord", e.CommandArgument)
Dim sb As New System.Text.StringBuilder()
Dim connectionString As String = ConfigurationManager.ConnectionStrings("CS_Connection").ConnectionString
Using connection As New SqlConnection(connectionString)
Dim myCommand As New SqlCommand("View", connection)
myCommand.CommandType = CommandType.StoredProcedure
Dim sqlRecord As SqlParameter = New SqlParameter("#Name", SqlDbType.VarChar)
sqlRecord.Value = Session("SvarRecord")
myCommand.Parameters.Add(sqlRecord)
connection.Open()
Using reader As SqlClient.SqlDataReader = myCommand.ExecuteReader
REM Read() returns True if data can be read
If reader.Read() Then
REM IsDbNull checks if given column (by ordinal) contains DbNull.
REM You need it because you can not convert DbNull to a number. As alternative
REM you may read it as object and compare by yourself.
If reader.IsDBNull(0) Then
Response.Redirect("Entry.Aspx")
REM We are sure it is not DbNull and we can assume it is an integer
ElseIf reader.GetInt32(0) = 0 Then
Response.Redirect("Negatives.Aspx")
ElseIf reader.GetInt32(0) = 1 Then
Response.Redirect("PrevEntry.Aspx")
End If
End If
reader.Close()
End Using
connection.Close()
connection.Dispose()
End Using
You're comparing HasRows property (a Boolean that indicates if recordset is empty or not), not value returned from your stored procedure.
Change your code to:
Using reader As SqlClient.SqlDataReader = myCommand.ExecuteReader
Rem Read() returns True if data can be read
If reader.Read() Then
Rem IsDbNull checks if given column (by ordinal) contains DbNull.
Rem You need it because you can not convert DbNull to a number.
Rem As alternative you may read it as object and compare by yourself.
If reader.IsDbNull(0) Then
Response.Redirect("Entry.Aspx")
Rem We are sure it is not DbNull and we can assume it is an integer
ElseIf reader.GetInt32(0) = 0 Then
Response.Redirect("Negatives.Aspx")
ElseIf reader.GetInt32(0) = 1 Then
Response.Redirect("PrevEntry.Aspx")
End If
End If
End Using
Here I assume your stored procedure returns an integer value. If it's not you can get/compare with right value or convert it to integer. First case (same for the other If):
ElseIf reader.GetString(0) = "0" Then
Second case:
ElseIf Convert.ToInt32(reader.GetObject(0)) = 0 Then
Last note about your code, as suggested by Jhon in his comment your code may fail for an unlimited number of reasons, you'd better to always wrap disposable objects in a Using statement like this:
Dim connectionString As String =
ConfigurationManager.ConnectionStrings("CS_Connection").ConnectionString)
Using connection As New SqlConnection(connectionString)
Rem Put here all code that uses connection
End Using
This will ensure connection and other shared (and limited!) resources will always be released even in case of error. Edit: compiler won't complain about comparison of a Boolean and a String because you didn't set OPTION STRICT to ON (see #Tim's answer for more details).
First, use the Using-statement to dispose/close the connection and anything else implementing IDisposable even on error. Second, you should really set OPTION STRICT to on globally, then this will not compile which is a good thing:
If reader.HasRows = "0" Then
The problem with that code is that HasRows is a Boolean but you are comparing it with a String. That would result in a compiler error normaly, but OPTION STRICT off allows it. The Boolean will be converted to a String implicitely. So this comparison seems to work but it does not.
Actually you have to read the field, you can use the Get... methods:
If reader.HasRows Then
If reader.IsDBNull(0) Then
Response.Redirect("Entry.Aspx")
ElseIf reader.GetInt32(0) = 1 Then
Response.Redirect("PrevEntry.Aspx")
ElseIf reader.GetInt32(0) = 0 Then
Response.Redirect("Negatives.Aspx")
End If
End If

SqlDataReader - Multiple connections

I've written a console app in VB.NET to do some database work and a strange runtime error has arisen...
Here's the main code:
Sub Main(ByVal args() As String)
Try
user = args(0)
batchID = args(1)
GetBatchRevision()
'batchRev = 1
Dim getTestScripts As SqlCommand = New SqlCommand("GetTestScriptsInTestBatch", cs)
getTestScripts.CommandType = CommandType.StoredProcedure
Dim batchIDParam As SqlParameter = getTestScripts.Parameters.Add("#batchID", SqlDbType.Int, 4)
Dim batchRevParam As SqlParameter = getTestScripts.Parameters.Add("#batchRev", SqlDbType.Int, 4)
'batchIDParam.Value = 1
'batchRevParam.Value = 1
batchIDParam.Value = batchID
batchRevParam.Value = batchRev
Console.WriteLine(batchID & " " & batchRev)
Console.WriteLine(cs.State)
Console.ReadLine()
Using cs
cs.Open()
Dim reader As SqlDataReader = getTestScripts.ExecuteReader(CommandBehavior.CloseConnection)
While reader.Read()
Console.WriteLine("Executing Test Script " & reader("ScriptID").ToString() & " Revision " & reader("ScriptRev").ToString)
End While
Console.ReadLine()
End Using
Catch ex As Exception
End Try
End Sub
GetBatchRevision:
Private Sub GetBatchRevision()
Using cs
Dim GetNewestRev As New SqlCommand("SELECT Max(BatchRev) FROM TestBatch WHERE BatchID=" & batchID, cs)
cs.Open()
Dim reader As SqlDataReader = GetNewestRev.ExecuteReader(CommandBehavior.CloseConnection)
reader.Read()
If Not IsDBNull(reader(0)) Then
batchRev = reader(0).ToString()
End If
End Using
End Sub
batchRev and batchID are both global variables within the module.
Behaviorally:
The app prints out "1" (user input), "1" (database result), "0" (enum of Closed connection)
When I press Enter to get past the first Console.ReadLine(), the app simply closes out.
If I comment out GetBatchRevision and directly set batchRev = 1, I get the above result as well as "Executing Test Script 1 Revision 52", "Executing Test Script 2 Revision 66" which are the expected results from the stored procedure GetTestScriptsInTestBatch.
The global variable declarations are as follows:
Private batchID As String
Private batchRev As String
Any ideas why GetBatchRevision() causes the app to crash? By itself (removing the stored proc part of the code), it executes just fine. My initial guess was that there was a hanging connection, but ending a "using" block is supposed to close a SQL connection as well as any open readers associated with said connection (as mentioned before, cs.State returns 0).
Your problem is on these lines:
reader.Read()
If Not IsDBNull(reader(0)) Then
reader.Read() is probably returning false; yet you try to access reader(0). Boom!
You should change it to:
IF reader.Read() AndAlso Not IsDBNull(reader(0)) Then
'' etc
End If
It looks like cs is also a global variable. This is a bad idea. .Net data access works a lot better when you're using a new connection each time. You're probably fine in this app, but you're setting up some bad habits. Instead, load your connection string as a global variable and use that when creating your connections.
Next up, there's no reason for GetBatchRevision() to talk to global variables. Have it accept an argument and return it's result instead. And of course I can't overlook the sql injection issue because you concatentate the batchid to the end of your string. Here's the new version of the function after fixing those errors:
Private Function GetBatchRevision(ByVal BatchID As String) As String
Using cn As New SqlConnection(cs), _
GetNewestRev As New SqlCommand("SELECT Max(BatchRev) FROM TestBatch WHERE BatchID= #BatchID", cn)
GetNewestRev.Parameters.Add("#Batch", SqlDbType.Int).Value = Convert.ToInt32(BatchId)
cn.Open()
Return GetNewestRev.ExecuteScalar().ToString()
End Using
End Function
This can get even better if you keep BatchRev and BatchID as int's rather than strings internally.

Resources