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 ....
Related
I'm unable to find a solution online for my question. If it is even possible, how do I write an SQL Insert statement that uses parameter values as well as selecting a value from another table.
Example:
"INSERT INTO Users (user_name, user_csn, user_adid, user_contact, user_adminpriviledge, user_datestart, user_active, user_team)
VALUES (#username, #usercsn, #useradid, #usercontact, #userauth, #userstart, #useractive, #userteam = (SELECT team_id FROM teaminfo WHERE team_name = '" & ddlAddTeam.SelectedValue & "'))"
I understand that the example is wrong, just trying my best to represent what I'm looking for in code.
Also another question would be regarding aliasing and datareaders. I seem to be unable to do "reader("column_name")" for aliased column names?
Example:
query = "SELECT u.*, t.team_name FROM Users u
JOIN teaminfo t ON u.user_team = t.team_id WHERE user_csn = '" & GV.userCSN & "'"
I tried to use
reader("u.user_name")
but failed as well.
You need other syntax of insert operation: INSERT INTO ... SELECT ... FROM ...:
INSERT INTO Users (user_name, user_csn, user_adid, user_contact, user_adminpriviledge, user_datestart, user_active, user_team)
SELECT #username, #usercsn, #useradid, #usercontact, #userauth, #userstart, #useractive, team_id --<--not here's your column
FROM teaminfo
WHERE team_name = #param
Also, it looks like it's .NET (C# or VB code), so you you are prone to SQL injection concatenating you string with parameters!
In my SQL I already put #param in proper place, then with SqlCommand you are probably using, you have to call method Addon SqlCommand.Paramteres collection, and then supplly it with value of ddlAddTeam.SelectedValue.
Try this code:
Using connection = New SqlConnection("connString")
Using com = New SqlCommand
com.Connection = connection
com.CommandText = "INSERT INTO Users (user_name, user_csn, user_adid, user_contact, user_adminpriviledge, user_datestart, user_active, user_team)
Select #username, #usercsn, #useradid, #usercontact, #userauth, #userstart, #useractive, team_id --<--Not here's your column
From teaminfo
Where team_name = #param"
com.Parameters.Add("#param", SqlDbType.VarChar).Value = ddlAddTeam.SelectedValue
connection.Open()
End Using
End Using
And for column alises: in data reader you use column aliases without table name (u before the dot in ou example). Try to give aliases to all your columns to avoid such problems.
The data source for an INSERT statement can be a SELECT statement—see the <dml_table_source> part of the statement definition at the linked page—and a SELECT statement can include parameters in the select list. Here's a simple example:
declare #Target table (Id bigint, Datum char(1));
declare #Source table (Id bigint);
declare #Datum char(1) = 'X';
insert #Source values (1);
insert #Target
select
Id = S.Id, -- Value from another table
Datum = #Datum -- Parameter
from
#Source S;
There are more examples at the page linked above; scroll down to the "Inserting Data From Other Tables" section header.
Also, if you're going to build a query in (C#?) code as you've shown in your example, you should really pass any arguments as parameters rather than trying to build them directly into the query text. Read up on SQL injection attacks to see why.
Your INSERT query should be like
"INSERT INTO Users (user_name, user_csn, user_adid, user_contact, user_adminpriviledge, user_datestart, user_active, user_team)
VALUES (#username, #usercsn, #useradid, #usercontact, #userauth, #userstart, #useractive, (SELECT team_id FROM teaminfo WHERE team_name = #userteam ))"
Second when fetching from reader it should be like :
reader("user_name") // I am not sure about this. You can put break point and open the object in watch window
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 wrote my first sql Server returning table UDF since thought was better than using a SP
but, while can easily retrieve the result from sql server.. I can't get result calling it from classic ASP ADO
UDF is as follows:
CREATE FUNCTION dbo.udf_Alert
(
#I nvarchar (30),
#L nvarchar (10)
)
RETURNS TABLE AS RETURN
(
SELECT a.Message, a.Height, a.backgroundColor, a.isFree
from Advice a
join ActiveMessages b on b.MessageID=a.MessageID
join Items i on b.ItemID=i.ItemID
join Sellers s on i.UserID=s.UserID
join Users u on u.UID=s.UID
WHERE
(i.ItemID=#I and a.Active='1' and b.Active='1' and i.active='1' and i.Show='1' and CHARINDEX('ALERT',u.Modules)>0
and a.ValidFrom<GETDATE() and a.ValidTo>GETDATE() and u.PaidUntil>GETDATE() and charindex(#L,a.Languages)>-1 or charindex('all',a.Languages)>-1 )
UNION ALL
SELECT a.Message, a.Height, a.backgroundColor, a.isFree
FROM Advice a, Users u
WHERE u.isFree='1' and a.isFree='1' and (CHARINDEX(#L,a.Languages)>-1 or Charindex('all',a.Languages)>-1)
)
and I can easily execute from SSMS calling
Select * from dbo.udf_Alert('281F50246','fr')
But I have to embed into a classic ASP routine but I've not found the way to do it..
tried the SP method.. but I got error when try to set the parameters:
here what I tried:
sql="Select dbo.udf_Alert('xx','yy')"
dim cmdA
set cmdA = Server.CreateObject("ADODB.Command")
cmdA.ActiveConnection= cn
cmdA.CommandType=4
cmdA.CommandText=sql
cmda.Parameters.Append cmdA.CreateParameter("fd", nvarchar, adParamInput,30, itemID)
cmda.Parameters.Append cmdA.CreateParameter("fde", nvarchar, adParamInput,10, LanguageID)
' cmdA.Parameters("#I")=ItemID '<-----ERRROR HERE
' cmdA.Parameters("#L")=LanguageID
set rs=cmdA.Execute()
so I tried set Parametrs in other way.. but got same result:
ADODB.Command error '800a0bb9'
Arguments are of the wrong type, are out of acceptable range, or are in conflict with one another.
Can suggest some advice?
Thanks
Sergio
We tend to create stored procedure "wrappers" for UDFs on a one-to-one basis which can be called directly from web code. For your example, this might look like
CREATE PROCEDURE [dbo].[pr_Alert]
#I nvarchar (30),
#L nvarchar (10)
AS
SELECT * FROM testDb.dbo.udf_Alert(#I, #L)
RETURN
Your commandText would then be simply the name of the wrapper SP, "pr_Alert".
Hope this helps.
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.
I have Change Data Capture (CDC) activated on my MS SQL 2008 database and use the following code to add a new tabel to the data capture:
EXEC sys.sp_cdc_enable_table
#source_schema ='ordering',
#source_name ='Fields',
#role_name = NULL,
#supports_net_changes = 0;
However, whenever I try to select the changes from the tracking tables using the sys.fn_cdc_get_min_lsn(#TableName) function
SET #Begin_LSN = sys.fn_cdc_get_min_lsn('Fields')
I always get the zero value.
I tried adding the schema name using the following spelling:
SET #Begin_LSN = sys.fn_cdc_get_min_lsn('ordering.Fields')
but this didn't help.
My mystake was to assume that sys.fn_cdc_get_min_lsn() accepts the table name. I was mostly misguided by the examples in MSDN documentation, probably and didn't check the exact meaning of the parameters.
It turns out that the sys.fn_cdc_get_min_lsn() accepts the capture instance name, not table name!
A cursory glance at my current capture instances:
SELECT capture_instance FROM cdc.change_tables
returns the correct parameter name:
ordering_Fields
So, one should use underscore as schema separator, and not the dot notation as it is common in SQL Server.
I know this is mostly already explained in this post but I thought I would put together my evenings journey through CDC
This error:
"An insufficient number of arguments were supplied for the procedure or function cdc..."
Is probably caused by your low LSN being 0x00
This in turn might be because you put the wrong instance name in with fn_cdc_get_min_lsn.
Use SELECT * FROM cdc.change_tables to find it
Lastly make sure you use binary(10) to store your LSN. If you use just varbinary or binary, you will again get 0x00. This is clearly payback for me scoffing at all those noobs using varchar and wondering why their strings are truncated to one character.
Sample script:
declare #S binary(10)
declare #E binary(10)
SET #S = sys.fn_cdc_get_min_lsn('dbo_YourTable')
SET #E = sys.fn_cdc_get_max_lsn()
SELECT #S, #E
SELECT *
FROM [cdc].[fn_cdc_get_net_changes_dbo_issuedToken2]
(
#S,#E,'all'
)
The above answer is correct. Alternatively you can add an additional parameter capture_instance to the cdc enable
EXEC sys.sp_cdc_enable_table
#source_schema ='ordering',
#source_name ='Fields',
#capture_instance = 'dbo_Fields'
#role_name = NULL,
#supports_net_changes = 0;
then use the capture_instance string in the min_lsn function
SET #Begin_LSN = sys.fn_cdc_get_min_lsn('dbo_Fields')
will return the first LSN, and not 0x00000000000000000000.
This is partiularly useful when trying to solve the error
"An insufficient number of arguments were supplied for the procedure or function cdc..." from SQL when calling
cdc_get_net_changes_Fields(#Begin_LSN, sys.fn_cdc_get_max_lsn(), 'all')
Which simply means "LSN out of expected range"