How do I associate Parameters to Command objects in ADO with VBScript? - vbscript

I have been working an ADO VBScript that needs to accept parameters and incorporate those parameters in the Query string that gets passed the the database. I keep getting errors when the Record Set Object attempts to open. If I pass a query without parameters, the recordset opens and I can work with the data. When I run the script through a debugger, the command object does not show a value for the parameter object. It seems to me that I am missing something that associates the Command object and Parameter object, but I do not know what. Here is a bit of the VBScript Code:
...
'Open Text file to collect SQL query string'
Set fso = CreateObject("Scripting.FileSystemObject")
fileName = "C:\SQLFUN\Limits_ADO.sql"
Set tso = fso.OpenTextFile(fileName, FORREADING)
SQL = tso.ReadAll
'Create ADO instance'
connString = "DRIVER={SQL Server};SERVER=myserver;UID=MyName;PWD=notapassword; Database=favoriteDB"
Set connection = CreateObject("ADODB.Connection")
Set cmd = CreateObject("ADODB.Command")
connection.Open connString
cmd.ActiveConnection = connection
cmd.CommandText = SQL
cmd.CommandType = adCmdText
Set paramTotals = cmd.CreateParameter
With paramTotals
.value = "tot%"
.Name = "Param1"
End With
'The error occurs on the next line'
Set recordset = cmd.Execute
If recordset.EOF then
WScript.Echo "No Data Returned"
Else
Do Until recordset.EOF
WScript.Echo recordset.Fields.Item(0) ' & vbTab & recordset.Fields.Item(1)
recordset.MoveNext
Loop
End If
The SQL string that I use is fairly standard except I want to pass a parameter to it. It is something like this:
SELECT column1
FROM table1
WHERE column1 IS LIKE ?
I understand that ADO should replace the "?" with the parameter value I assign in the script. The problem I am seeing is that the Parameter object shows the correct value, but the command object's parameter field is null according to my debugger.

I know this is old, but for anyone still fiding this (like I did via google):
If you're using Stored Procedures:
set cmd = Server.CreateObject("ADODB.Command")
with cmd
.ActiveConnection = db_connection
.CommandText = "stored_procedure_name"
.CommandType = adCmdStoredProc
.Parameters.Append .CreateParameter("#Parameter1",adInteger,adParamInput,,1)
.Parameters.Append .CreateParameter("#Parameter2",adVarChar,adParamInput,100,"Up to 100 chars")
.Parameters.Append .CreateParameter("#Parameter3",adBoolean,adParamInput,,true)
.Parameters.Append .CreateParameter("#Parameter4",adDBTimeStamp,adParamInput,,now())
end with
set rs = cmd.execute
'do stuff with returned results from select or leave blank if insert/delete/etc stored procedure
set rs = nothing
set cmd = nothing
If not, I beleive you change the .CommandText to your SQL statement with questions marks in place and your Parameters must follow the same order.
See http://www.devguru.com/technologies/ado/quickref/command_createparameter.html
For a breakdown of what values you're passing with CreateParameter, as well as a list of types and their descriptions.

After you create the parameter, you have to append it to the Command object's Parameters collection before you execute the Command:
Set paramTotals = cmd.CreateParameter
With paramTotals
.Value = "tot%"
.Name = "Param1"
End With
cmd.Parameters.Append paramTotals
You may also need to specify the Type and Size properties for the Parameter. Generally, I use the arguments of the CreateParameter function to set all the necessary properties in one line:
Set paramTotals = cmd.CreateParameter("Param1", adVarChar, adParamInput, 30, "tot%")
cmd.Parameters.Append paramTotals

I never got CreateParameter doing what I wanted. Proper parameterization is a necessity to avoid SQL injection, but CreateParameter is a complete PITA. Thankfully, there's an alternative: Command.Execute takes in parameters directly.
dim cmd, rs, rows_affected
set cmd = Server.createObject("adodb.command")
cmd.commandText = "select from Foo where id=?"
set cmd.activeConnection = someConnection
set rs = cmd.execute(rows_affected, Array(42))
It's much nicer when wrapped up in a proper abstraction. I wrote my own database class wrapping ADODB.Connection so I wouldn't have to do all of this manually. It relies a bit on other custom classes, but the gist of it should be apparent:
class DatabaseClass
' A database abstraction class with a more convenient interface than
' ADODB.Connection. Provides several simple methods to safely query a
' database without the risk of SQL injection or the half-dozen lines of
' boilerplate otherwise necessary to avoid it.
'
' Example:
'
' dim db, record, record_set, rows_affected
' set db = Database("/path/to/db")
' set record = db.get_record("select * from T where id=?;", Array(42))
' set record_set = db.get_records("select * from T;", empty)
' rows_affected = db.execute("delete from T where foo=? and bar=?",
' Array("foo; select from T where bar=", true))
private connection_
' An ADODB connection object. Should never be null.
private sub CLASS_TERMINATE
connection_.close
end sub
public function init (path)
' Initializes a new database with an ADODB connection to the database at
' the specified path. Path must be a relative server path to an Access
' database. Returns me.
set connection_ = Server.createObject ("adodb.connection")
connection_.provider = "Microsoft.Jet.OLEDB.4.0"
connection_.open Server.mapPath(path)
set init = me
end function
public function get_record (query, args)
' Fetches the first record returned from the supplied query wrapped in a
' HeavyRecord, or nothing if there are no results.
dim data: set data = native_recordset(query, args)
if data.eof then
set get_record = nothing
else
set get_record = (new HeavyRecordClass).init(data)
end if
end function
public function get_records (query, args)
' Fetches all records returned from the supplied query wrapped in a
' RecordSet (different from the ADODB recordset; implemented below).
set get_records = (new RecordSetClass).init(native_recordset(query, args))
end function
public function execute (query, args)
' Executes the supplied query and returns the number of rows affected.
dim rows_affected
build_command(query).execute rows_affected, args
execute = rows_affected
end function
private function build_command (query)
' Helper method to build an ADODB command from the supplied query.
set build_command = Server.createObject("adodb.command")
build_command.commandText = query
set build_command.activeConnection = connection_
end function
private function native_recordset (query, args)
' Helper method that takes a query string and array of arguments, queries
' the ADODB connection, and returns an ADODB recordset containing the
' result.
set native_recordset = build_command(query).execute( , args) ' Omits out-parameter for number of rows
end function
end class

Related

ADO recordset not populated by ADO command execute method

I am doing some clean up to protect from SQL injection attacks happening in a older internal website that uses ASP. Here's the gist of it all in code...
Database connection is setup in a separate asp file named connect.asp
<%
on error resume next
Set DB = Server.CreateObject("ADODB.Connection")
DB.CommandTimeout = 180
DB.ConnectionTimeout = 180
connStr = "Provider=SQLOLEDB;Data Source=xxx-xxxx-xxxxxx;Initial Catalog=xxxx;Persist Security Info=True;User ID=xxxxx;Password=xxxxxxxxxxxx;"
DB.Open connStr
' Check DB connection and go to Error Handler is error exists
if DB.state=0 then
Response.Write "<p>Cannot connect to database.</p>"
TrapError Err.description
Response.end
end if
%>
This works and the db connections is opened.
I have a file named DBFunctions.asp that I use to sort of map functions to stored procedures and their parameters. I am trying to use the function below to return a ADO recordset to another asp front end page.
Function GetFacilityByFID(fid)
set rs = server.CreateObject("ADODB.Recordset")
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = DB
cmd.CommandText = "GetFacilityByFID"
cmd.CommandType = adCmdStoredProc
cmd.Parameters.Append cmd.CreateParameter("#FID", adVarChar, adParamInput, 20)
cmd("#FID") = fid
Set rs = cmd.Execute
Set GetFacilityByFID = rs
End Function
Here is the code from the calling front end asp page, facDetail.asp
<%
Dim FID, FCBI, Error
FID = Request("FID")
FCBI = Request("FCBI")
' Check DB connection and go to Error Handler is error exists
if DB.state=0 then
Response.Write "<p>Cannot connect to database.</p>"
TrapError Err.description
Response.end
else
if FID then
Set RS = GetFacilityByFID(FID)
elseif FCBI then
Set RS = GetFacilityByFCBI(FCBI)
end if
if RS.EOF then
Response.Write "<BR><p class=alert>No record found</p>"
response.End
end if
end if
%>
The calling page is displaying that there are no records returned
but the stored procedure works when executed in SSMS.
Updated code
Here's the SQL Code for the GetFacilityByFID stored procedure.
CREATE PROCEDURE [dbo].[GetFacilityByFID]
#FID varchar(20)
AS
BEGIN
SET NOCOUNT ON;
SELECT [FAC_CBI_NBR]
,[FAC_ID]
,[FAC_TYPE]
,[FAC_SUBTYPE]
,[FAC_REGION]
,[FAC_COST_CENTER]
,[FAC_SUPPLY_CODE]
,[FAC_UPLINE]
,[FAC_SERVICE]
,[FAC_LOCATION_NAME]
,[FAC_LOCAL_ADDR1]
,[FAC_LOCAL_ADDR2]
,[FAC_LOCAL_CITY]
,[FAC_LOCAL_STATE]
,[FAC_LOCAL_ZIP]
,[FAC_MAIL_ADDR1]
,[FAC_MAIL_ADDR2]
,[FAC_MAIL_CITY]
,[FAC_MAIL_STATE]
,[FAC_MAIL_ZIP]
,[FAC_COUNTRY]
,[FAC_PHONE]
,[FAC_FAX]
,[FAC_MANAGER]
,[FAC_CONTACT]
,[FAC_CONTACT_PHONE]
,[FAC_CONTACT_EXT]
,[FAC_CONTACT_EMAIL]
,[FAC_COMMENTS]
,[FAC_CHANGED_BY]
,[FAC_LAST_UPDATE]
,[FAC_MAILOUT]
,[FAC_CONTRACTION]
,[FAC_PROPERTY_CODE]
,[FAC_ATTN_TO]
FROM [cbid].[dbo].[FACILITY]
WHERE [FAC_ID]=#FID
END
GO
Can anyone tell me what is going wrong? I have been looking at this too long and have grown frustrated with it.
Any help will be greatly appreciated!
Edit:
Current Status of issue: getting the following from the ADO provider
Error Number: -2147217904 Error Desc: Procedure or function 'GetFacilityByFID' expects parameter '#FID', which was not supplied. Error Source: Microsoft OLE DB Provider for SQL Server
You need to specify the size.
From CreateParameter Method (ADO)
If you specify a variable-length data type in the Type argument, you
must either pass a Size argument or set the
Size
property of the Parameter object before appending it to the
Parameters collection; otherwise, an error occurs.
cmd.Parameters.Append cmd.CreateParameter("#FID", adVarChar, adParamInput, Len(fid))
Before going any further, the first thing is to confirm if your stored procedure GetFacilityByFID actually returns a Recordset? Most likely it does not. If it only return a single string value, you should modify Function GetFacilityByFID(fid) to something like below:
Function GetFacilityByFID(fid)
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = DB
cmd.CommandText = "GetFacilityByFID"
cmd.CommandType = adCmdStoredProc
cmd.Parameters.Append cmd.CreateParameter("#returnVal", adVarChar, adParamOutput, 255, "")
cmd.Parameters.Append cmd.CreateParameter("#FID", adVarChar, adParamInput)
cmd("#FID") = fid
cmd.Execute
GetFacilityByFID = cmd("#returnVal")
End Function
I rewrote the function using some of the examples I found and some of LankyMart's suggestions. Thanks everyone for your help.
Here's the working code...
Function GetFacilityByFID(fid)
Set rs = server.CreateObject("ADODB.Recordset")
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = DB
cmd.CommandText = "GetFacilityByFID"
cmd.CommandType = adCmdStoredProc
cmd.CommandTimeout = 900
set prm = cmd.CreateParameter("#FID",adVarChar, adParamInput, 20, fid)
cmd.Parameters.Append prm
rs.CursorLocation = adUseClient
rs.Open cmd, , adOpenForwardOnly, adLockReadOnly
Set GetFacilityByFID = rs
On Error GoTo 0
End Function
The way you're setting the parameters looks no right, try this instead:
Dim param
Set param = cmd.CreateParameter("#FID", adVarChar, adParamInput)
cmd.Parameters.Append param
param.Value = fid
You can read more here: https://msdn.microsoft.com/pt-br/library/ms675860(v=vs.85).aspx
Hope it helps

VbScript ADODB.RecordSet RecordCount returns -1

My question today is a rather simple one. What I have is a VB Module that contains the code to return me an ADODB.RecordSet object with records fetched from a SQL Query that has been executed. It works like this:
sSql_SerCheck = "SELECT DISTINCT Serial FROM dbo.WipReservedSerial WHERE Serial LIKE '" & serialTempSearch
sSql_SerCheck = sSql_SerCheck & "' ORDER BY Serial DESC "
dbGetRecordSet(sSql_SerCheck)
Then the results sit in object rs that is accessed like the following
temp = rs(0) 'For the value at the first column for the first record
rs.MoveNext 'This moves to the next record in the record set
Now what I am trying to do here is to the number of records contained within this recordset object. Now I did some research on the class and found there is a
RecordCount att.
So what I want to do is simple:
if( rs.RecordCount > 0) then
serCheck1 = rs(0)
MsgBox serCheck1
end if
The problem is my RecordCount returns -1. I found this article http://www.w3schools.com/asp/prop_rs_recordcount.asp that states that record count will return -1 for the following:
Note: This property will return -1 for a forward-only cursor; the actual count for a static or keyset cursor; and -1 or the actual count for a dynamic cursor.
Note: The Recordset object must be open when calling this property. If this property is not supported it will return -1.
How do I get this object to return the correct number of records??
The code for the VB Module is added below:
Public cn, rs
'Specify pSQL as SQL Statement
Function dbGetRecordset(sSql)
dbCloseConnection()
Set cn = CreateObject("ADODB.Connection")
cn.CommandTimeout = 600
cn.Open(Conn & SystemVariables.CodeObject.CompanyDatabaseName)
Set rs = CreateObject("ADODB.Recordset")
rs.Open sSql, cn, 3, 3
End Function
As your rs.RecordCount > 0 just checks whether the recordset is not empty, you can avoid .Recordcount (and all it's problems) by testing for Not rs.EOF
Don't trust secondary sources; the MS docs contain "... and either -1 or the actual count for a dynamic cursor, depending on the data source". So maybe your provider is to blame. In that case (or when you really need a specific number), a SELECT COUNT() would be a workaround
Don't use magic numbers as in rs.Open sSql, cn, 3, 3, but define (and doublecheck) your Consts like adOpenStatic, adLockOptimistic, ...
From Help
Set oRs = New ADODB.Recordset
oRs.CursorLocation = adUseClient
oRs.Open sSQL, sConn, adOpenStatic, adLockBatchOptimistic, adCmdText
Help has a full description of Cursors in What is a Cursor (ADODB Programmers Guide - Windows Software Development Kit).
You'll burn resources either locally or on the server to get a record count. If you are going through the data anyway, just count them.
This is how to go through a recordset one at a time.
Do While not .EOF
Outp.writeline .Fields("Txt").Value
.MoveNext
Loop
Set connection = CreateObject("adodb.connection")
connection.open "Driver=your driver details"
If connection.State = 1 Then
Set myRecord = CreateObject("ADODB.recordset")
sql= "select * from...."
myRecord.Open sql, connection
i = 0
Do While Not myRecord.EOF
i=i+1
myRecord.MoveNext
loop
msgbox "RecordCount="&i
myRecord.Close
END IF
set myRecord = nothing
set Connection = nothing
I was finding that the vbscript version of this was flaky... the best option seemed to use a CursorLocation of 3.. vbscript didn't seem to understand adUseClient.
set conn=CreateObject("ADOdb.connection")
conn.CursorLocation = 3
conn.open DSNQ
set rs = conn.execute("select * from sometable order by 1 desc")
msgbox (rs.RecordCount & " records" )```

vbscript ADO Recordset Update

I am trying to update paths in access mdb database using ADO and vb6 without success.
The scripts are below. The line Rs1(columnName) = Replace( Rs1(columnName),oldPath,newPath) causes vbscript runtime err invalid use of Null.
Put simply, I want to update all tables that have strings like \\server2 to \\DBSE-46\. I
am running the script on win7 64bit as
c:\windows\syswow64\cscript.exe C:\SQLTest\HarishScripts\DatabaseAccessProg6.vbs >> C:\SQLTest\HarishScripts\DatabaseAccessProg6.txt
Option Explicit
WScript.Echo "START of ADO access program...."
Dim DBpath
Dim tableName
Dim columnName
Dim oldPath
Dim newPath
'#######################################################################################
' Set all external variables here...
DBpath = "C:\DBTest;"
tableName = "Test"
columnName = "Path"
oldPath = "\\SERVER2\"
newPath = "\\DBSE-46\"
'#######################################################################################
Dim Rs1
Set Rs1 = CreateObject("ADODB.Recordset")
Dim i
Dim AccessConnect
AccessConnect = "Driver={Microsoft Access Driver (*.mdb)};" & _
"Dbq=MedDataSource.mdb;" & _
"DefaultDir=" & DBpath & _
"Uid=Admin;Pwd=;"
'--------------------------
' Recordset Object Method
'--------------------------
' Recordset Open Method #4: Open w/o Connection & w/Connect String
Dim sqlStmt
sqlStmt = "SELECT * FROM " & tableName
' use LockTypeEnum.adLockOptimistic = 3. This allows update of the recordset.
Rs1.LockType = 3
Rs1.Open sqlStmt, AccessConnect
Do Until Rs1.EOF
'WScript.Echo Rs1("Path")
if (Rs1(columnName) = NULL) Then
End If
Rs1(columnName) = Replace( Rs1(columnName),oldPath,newPath)
Rs1.MoveNext
Loop
' Close the recordset...
Rs1.Close
Set Rs1 = Nothing
WScript.Echo "..."
WScript.Echo "..."
WScript.Echo "DONE!"
Use the IsNull function to check for Null values. In my older local .chm there is even a paragraph
Use the IsNull function to determine whether an expression contains a
Null value. Expressions that you might expect to evaluate to True
under some circumstances, such as If Var = Null and If Var <> Null,
are always False. This is because any expression containing a Null is
itself Null, and therefore, False.
Work on your understanding of the If clause. In
if (Rs1(columnName) = NULL) Then
End If
Rs1(columnName) = Replace( Rs1(columnName),oldPath,newPath)
the last statement will be executed regardless of the content of Rs1(columnName). So do
if Not IsNull(Rs1(columnName)) Then
Rs1(columnName) = Replace( Rs1(columnName),oldPath,newPath)
End If

Classic ASP disconnected recordsets issue

So, I've been asked to update an old Classic ASP website. It did not use parameterized queries and there was very little input validation. To simplify things I wrote a helper function that opens a connection to the database, sets up a command object with any parameters, and creates a disconnected recordset [I think!?! :)] Here's the code:
Function GetDiscRS(DatabaseName, SqlCommandText, ParameterArray)
'Declare our variables
Dim discConn
Dim discCmd
Dim discRs
'Build connection string
Dim dbConnStr : dbConnStr = "Provider=Microsoft.Jet.OLEDB.4.0;" & _
"Data Source=" & rootDbPath & "\" & DatabaseName & ".mdb;" & _
"Persist Security Info=False"
'Open a connection
Set discConn = Server.CreateObject("ADODB.Connection")
discConn.Open(dbConnStr)
'Create a command
Set discCmd = Server.CreateObject("ADODB.Command")
With discCmd
Set .ActiveConnection = discConn
.CommandType = adCmdText
.CommandText = SqlCommandText
'Attach parameters to the command
If IsArray(ParameterArray) Then
Dim cnt : cnt = 0
For Each sqlParam in ParameterArray
discCmd.Parameters(cnt).Value = sqlParam
cnt = cnt + 1
Next
End If
End With
'Create the Recordset object
Set discRs = Server.CreateObject("ADODB.Recordset")
With discRs
.CursorLocation = adUseClient ' Client cursor for disconnected set
.LockType = adLockBatchOptimistic
.CursorType = adOpenForwardOnly
.Open discCmd
Set .ActiveConnection = Nothing ' Disconnect!
End With
'Return the Recordset
Set GetDiscRS = discRS
'Cleanup
discConn.Close()
Set discConn = Nothing
discRS.Close() ' <=== Issue!!!
Set discRs = Nothing
Set discCmd = Nothing
End Function
My problem is that if I call discRS.Close() at the end of the function, then the recordset that is returned is not populated. This made me wonder if the recordset is indeed disconnected or not. If I comment that line out everything works properly. I also did some Response.Write() within the function using discRS values before and after setting ActiveConnection = Nothing and it properly returned the recordset values. So it seems to be isolated to discRS.Close().
I found an old article on 4guysfromrolla.com and it issues the recordset Close() in the function. I've seen the same thing on other sites. I'm not sure if that was a mistake, or if something has changed?
Note: I'm using IIS Express built into Visual Studio Express 2013
Disconnected recordset as far as I know refers to a recordset populated manually, not from database, e.g.used as multi dimensional array or kind of hash table.
So what you have is not a disconnected recordset since it's being populated from database, and by disposing its connection you just cause your code to not work properly.
Since you already have Set discConn = Nothing in the code you don't have to set it to nothing via the recordset or command objects, it's the same connection object.
To sum this all up, you should indeed get rid of tho following lines in your code:
Set .ActiveConnection = Nothing ' Disconnect!
discRS.Close() ' <=== Issue!!!
Set discRs = Nothing
Then to prevent memory leaks or database lock issues, you should close and dispose the recordset after actually using it in the code using the function e.g.
Dim oRS
Set oRS = GetDiscRS("mydatabase", "SELECT * FROM MyTable", Array())
Do Until oRS.EOF
'process current row...
oRS.MoveNext
Loop
oRS.Close ' <=== Close
Set oRS = Nothing ' <=== Dispose
To avoid all this hassle you can have the function return "real" disconnected recordset by copying all the data into newly created recordset. If relevant let me know and I'll come with some code.
In your function, you cannot close and clean up your recordset if you want it to be returned to the calling process.
You can clean up any connections and command objects, but in order for your recordset to be returned back populated, you simply do not close it or dispose of it.
Your code should end like this:
'Cleanup
discConn.Close()
Set discConn = Nothing
'discRS.Close()
'Set discRs = Nothing
'Set discCmd = Nothing
end function
In your code i can see:
Set .ActiveConnection = Nothing ' Disconnect!
So, this Recordset isn't already closed?
He is indeed using a disconnected recordset. I started using them in VB6. You set the connection = Nothing and you basically have a collection class with all the handy methods of a recordset (i.e. sort, find, filter, etc....). Plus, you only hold the connection for the time it takes to fetch the records, so back when Microsoft licensed their servers by the connection, this was a nice way to minimize how many userm were connected at any one time.
The recordset is completely functional, it's just not connected to the data source. You can reconnect it and then apply any changes that were made to it.
It was a long time ago, it seems that functionality has been removed.
You should use the CursorLocation = adUseClient. Then you can disconnect the recordset. I have created a function to add the parameters to command dictionary objects, and then return a disconnected recordset.
Function CmdToGetDisconnectedRS(strSQL, dictParamTypes, dictParamValues)
'Declare our variables
Dim objConn
Dim objRS
Dim Query, Command
Dim ParamTypesDictionary, ParamValuesDictionary
'Open a connection
Set objConn = Server.CreateObject("ADODB.Connection")
Set objRS = Server.CreateObject("ADODB.Recordset")
Set Command = Server.CreateObject("ADODB.Command")
Set ParamTypesDictionary = Server.CreateObject("Scripting.Dictionary")
Set ParamValuesDictionary = Server.CreateObject("Scripting.Dictionary")
Set ParamTypesDictionary = dictParamTypes
Set ParamValuesDictionary = dictParamValues
Query = strSQL
objConn.ConnectionString = strConn
objConn.Open
With Command
.CommandText = Query
.CommandType = adCmdText
.CommandTimeout = 15
Dim okey
For Each okey in ParamValuesDictionary.Keys
.Parameters.Append .CreateParameter(CStr(okey), ParamTypesDictionary.Item(okey) ,adParamInput,50,ParamValuesDictionary.Item(okey))
Next
.ActiveConnection = objConn
End With
objRS.CursorLocation = adUseClient
objRS.Open Command , ,adOpenStatic, adLockBatchOptimistic
'Disconnect the Recordset
Set objRS.ActiveConnection = Nothing
'Return the Recordset
Set CmdToGetDisconnectedRS = objRS
'Clean up...
objConn.Close
Set objConn = Nothing
Set objRS = Nothing
Set ParamTypesDictionary =Nothing
Set ParamValuesDictionary =Nothing
Set Command = Nothing
End Function

ADO Recordset Losing Its Data When Passed To Several Functions In Sequence (Access VBA)

I have a function that returns an ADO recordset, this in turn is used to feed subsequent functions. The first time it's passed to one of these functions, it runs fine; but in the next function, the recordset is at EOF. I assumed that my recordset was being passed By Ref, so tried using MoveFirst: this gives me an error because the cursor type is defaulted as forward only.
I tried the following:
Set it the parameter as By Val; this had no effect.
Created a copy of the recordset and used the original in the first function and the copy in the second; still the same problem (second is saying EOF, which implies that it's empty rather than at EOF; I can't imagine that the two recordsets are connected in some way, but perhaps).
Changed my DB code to use a different cursor type (adOpenStatic): This would actually be more correct for my application, but it gave very weird results. Namely, in a query that worked fine before, after the change, the first field was not being returned! This is kind of important, so I couldn't continue down this route.
Any ideas?
Main DB query function: GetData. Takes SQL and list of varchar parameters (if any) and returns recordset. It references dbOpen, which simply opens the connection with the supplied credentials.
Private Function GetData(ODBC As DBConnection, ByVal QuerySQL As String, Optional Parameters As Collection) As ADODB.Recordset
Dim DB As ADODB.Connection
Dim Query As ADODB.Command
Dim Parameter As ADODB.Parameter
Set DB = New ADODB.Connection
If dbOpen(DB, ODBC) Then
Set Query = New ADODB.Command
Query.ActiveConnection = DB
Query.CommandText = QuerySQL
If Not Parameters Is Nothing Then
For Each param In Parameters
Set Parameter = Query.CreateParameter(, adVarChar, adParamInput, Len(param), param)
Query.Parameters.Append Parameter
Next
Set Parameter = Nothing
End If
Set GetData = Query.Execute
' Uncommenting this seems to reset the data in GetData...
' A bit of a memory leak, but we assume VBA will take care of it :P
'dbClose DB
Else
MsgBox "Cannot connect to the database.", vbExclamation
Set GetData = Nothing
End If
End Function
I then do something like this:
Dim myRecords as ADODB.Recordset
' For the sake of argument, we assume this returns a nontrivial recordset
Set myRecords = GetData(someDSN, someSQL, someParameters)
Debug.Print myRecords.EOF ' Returns False
ID = writeHeader(myRecords)
Debug.Print myRecords.EOF ' Returns True
writeData ID, myRecords ' Breaks because at EOF
The writeHeader function will go through each record in the recordset, using MoveNext. However, if I pass myRecords by value or as a copy, then we still get the EOF problem. If I add MoveFirst at the end of the record iteration code in writeHeader, it will complain that I can't move to the beginning of such a recordset. If I change my GetData function so that Set GetData = Query.Execute is changed to:
Set GetData = New ADODB.Recordset
GetData.CursorType = adOpenStatic
GetData.Open Query
Then, when a query in my code is run -- which hasn't changed -- the first field is not a part of the recordset (!?) Say, for example, myRecords!ID is the first field: if this is referenced, I'll get a warning that said item isn't a member of the records (and it's not an enumerated member, either; so myRecords.Fields(1) just returns the next field in the query).
With a useful tip from #TimWilliams, I solved this by changing my GetData function to the following:
Private Function GetData(ODBC As DBConnection, ByVal QuerySQL As String, Optional Parameters As Collection) As ADODB.Recordset
Dim DB As ADODB.Connection
Dim Query As ADODB.Command
Dim Parameter As ADODB.Parameter
Dim Output As ADODB.Recordset
Set DB = New ADODB.Connection
If dbOpen(DB, ODBC) Then
Set Query = New ADODB.Command
Query.ActiveConnection = DB
Query.CommandText = QuerySQL
If Not Parameters Is Nothing Then
For Each param In Parameters
Set Parameter = Query.CreateParameter(, adVarChar, adParamInput, Len(param), param)
Query.Parameters.Append Parameter
Next
Set Parameter = Nothing
End If
Set Output = New ADODB.Recordset
Output.CursorType = adOpenStatic
Output.CursorLocation = adUseClient
Output.Open Query
Else
MsgBox "Cannot connect to the database.", vbExclamation
Set Output = Nothing
End If
Set GetData = Output
End Function
Then, before cursoring through the recordset, I use MoveFirst.

Resources