I am working in Classic ASP. I know there is a record that matches my simple SQL select query. It has the ' character ' in it. The code is as follows:
Fieldname = Replace(trim(Request.form("Fieldname")),"'","'", 1, 10)
'replace the "'" up to 10 times with the ' code to avoid SQL issues, LOL.
SQL = "select id,fieldname from table where fieldname='"&Trim(Fieldname)&"'"
set rs = server.createobject("adodb.recordset")
rs.open SQL, Application("conn"), 1, 1
If not rs.eof then
response.redirect "somepage.asp?QS=Fieldname_Exists_in_DB"
Else
'Sample.hold the value in a hidden input field and pass it to the next page
End If
The problem is, I know for a fact the fieldname and fieldname value is in the MS-SQL 2016 server table. I pull data from it all the time. The value in the database field contains the ' value as does the Replaced FORM Fieldname when it is compared to the SQL database field, so it should NOT pass the IF NOT RS.EOF question. Yet it passes every time.
What am I missing? I'm doing the exact same query in other places on this exact same app and it behaves as one would expect.
Tried to explain in the comments but as the point is being missed, I'll try to give you an example here.
Do not trust user input
Classic ASP server-side code that interacts with the ADODB Library doesn't have any notion of sanitised data. This means that any input that comes from the user via the Request object (like Request.Form("Fieldname")) should not be trusted.
Fieldname = Replace(trim(Request.form("Fieldname")),"'","'", 1, 10)
SQL = "select id,fieldname from table where fieldname='"&Trim(Fieldname)&"'"
This example is open to SQL Injection attacks and is generally bad practise and leads to security flaws that can be easily exploited with script tools readily available on the internet.
Manually sanitising data
Apart from the security flaws introduced, it also makes it harder to query data due to how SQL calls for strings and other data types need to be constructed (which varies from provider to provider). Having to account for the various combinations of characters that could be deemed dangerous or likely to break the query can be a cumbersome task and one seen far too often in the wild when ADODB already has a solution.
Parameterised Queries
The ADODB Library has an in-built object called ADODB.Command which takes all these hassles away.
Using the example in the question the same query can be written without the failings of manually sanitising data or executing SQL directly against user input.
Const adCmdText = 1
Const adVarWChar = 202
Const adParamInput = 1
Dim Fieldname, SQL, cmd, rs,
Fieldname = Trim(Request.Form("Fieldname") & "")
SQL = "SELECT id, fieldname FROM table WHERE fieldname = ?"
Set cmd = Server.CreateObject("ADODB.Command")
With cmd
.ActiveConnection = Application("conn")
.CommandType = adCmdText 'Also can use 1
.CommandText = SQL
Call .Append(.CreateParameter("#fieldName", adVarWChar, adParamInput, 255))
Set rs = .Execute(, Array(Fieldname))
End With
Set cmd = Nothing
If Not rs.EOF then
response.redirect "somepage.asp?QS=Fieldname_Exists_in_DB"
Else
'Sample.hold the value in a hidden input field and pass it to the next page
End If
Useful Links
A: Using METADATA to Import DLL Constants (shows an approach to using Named Constants that doesn't require adding your own Const declarations to the code).
Related
I'm maintaining an Access 365 database (32-bit) running on devices using Access 365 Runtime (32-bit) on Windows 10 & 11. The back-end uses Microsoft SQL Server Express (64-bit), version 15.0.4198.2, on AWS RDS. For one feature, the code uses ADODB 2.8 (the VBA reference is Microsoft ActiveX Data Objects 2.8 Library) to open a Recordset, connect to a table, and modify some fields.
The code was working just fine until I included a line to switch a boolean field from true to false. After this change, the code would throw error #-2147217864 with the description Row cannot be located for updating. Some values may have been changed since it was last read.. I isolated the code to a unit test and ensured that no other lines of code changed the recordset, but the code still threw the error.
Here's the unit test with some helper functions shown but not included:
Private Sub TestRelistingDataChangeProcess()
On Error GoTo TestFail
Dim itemSku As String
itemSku = "1234"
Dim verifySql As String
verifySql = StrFormat("SELECT failedImport FROM dbo.myTable WHERE SKU = '{0}'", itemSku)
Dim rsSql As String
rsSql = StrFormat("UPDATE dbo.myTable SET failedImport = 0 WHERE SKU = '{1}'", itemSku)
ExecuteCommandPassThrough rsSql
rsSql = "PARAMETERS SKU Text ( 255 ); SELECT * FROM myTable WHERE SKU=[SKU]"
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
cmd.ActiveConnection = GetCurrentConnection()
cmd.CommandText = rsSql
Dim param As ADODB.Parameter
Set param = cmd.CreateParameter(Name:="[SKU]", Type:=adLongVarChar, Value:=itemSku, Size:=Len(itemSku))
cmd.Parameters.Append param
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open cmd, , adOpenDynamic, adLockOptimistic
With rs
Debug.Print "1. Setting field to TRUE."
.Fields("failedImport") = True
.Update
Assert.IsTrue ExecuteScalarAsPassThrough(verifySql)
Debug.Print "2. Setting field to FALSE."
.Fields("failedImport") = False
.Update
Assert.IsFalse ExecuteScalarAsPassThrough(verifySql)
End With
Assert.Succeed
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
Resume TestExit
End Sub
Searching for information on this error led to many possibilities, not all of them in VBA. I was aware of issues with Access and SQL Server tables with nullable boolean fields, so I verified the SQL Server table had a default value for the field. I tried numerous CursorType and LockType combinations when opening the recordset. None worked.
What am I doing wrong that causes this error to be thrown? What can I do to change the code so that it works?
After serious searching and testing, I found this blog post which included this line from the [9 Nov 2009 8:49] Tonci Grgin post:
rsCustomers.Properties("Update Criteria").Value = adCriteriaKey
I didn't recognize the adCriteriaKey enum, so I searched, found, and read this MS documentation page. This enum family "specifies which fields can be used to detect conflicts during an optimistic update of a row of the data source with a Recordset object." Specifically, the adCriteriaKey value "detects conflicts if the key column of the data source row has been changed, which means that the row has been deleted."
Through some testing and debug statements, I learned the recordset I opened used adCriteriaUpdCols by default. This value "detects conflicts if any of the columns of the data source row that correspond to updated fields of the Recordset have been changed." For whatever reason, ADODB was identifying a conflict when there shouldn't be one. I wondered whether the bug had something to do with VBA using -1 as true where SQL Server uses 1, but that doesn't appear to be the case based on this SO post.
I also don't know why the previous version of code worked when changing the boolean field from false to true but not from true to false. Perhaps there is a way to trace into the ADODB code and determine exactly what's going wrong here, but I don't know how to do it yet. I've already spent HOURS on this bug, so I need to move on... :-)
As such, here's the line of code I added to make everything work:
Dim rs As ADODB.Recordset
Set rs = New ADODB.Recordset
rs.Open cmd, , adOpenDynamic, adLockOptimistic
rs.Properties("Update Criteria").Value = adCriteriaKey ' <----- NEW LINE HERE
Note that this line will only work for you if your table includes a primary key and you use it in your Recordset. Also, here's another forum post showing the adCriteriaKey saving the day.
I hope this writeup makes sense to others and helps save someone in the future some time! If nothing else, it was a good exercise for me. :-)
I need to create a pass-through query to SQL Server. I have a functioning pass-through query which only runs from the navigation bar or query design in Access, and it only creates a single record. I have dozens of records to insert into a SQL Server table based on values in an Access table. Thus, I need to loop through the Access table in code, and for each record there create the SQL string and execute it. I've got the looping working, just need the proper code to connect to the SQL database and execute the SQL string. Here's the ODBC Connect String in the pass-through query that works, with the PW and DB starred out: "ODBC;DSN=jobboss32; UID=support; PWD=****;DATABASE=****". The code below gives a Data Type Conversion Error.
Dim strFuturePTOSource, strPassThruInsert, strEmpName, strPTODate, strUpdated As String
Dim datPTODate As Date, lngPTOHours As Long
Dim rs As New ADODB.Recordset, q As QueryDef
strFuturePTOSource = "qryROBERT" 'temporary data set to test, change to actual after working
rs.Open strFuturePTOSource, CurrentProject.Connection, adOpenDynamic, adLockOptimistic
With rs
If Not .BOF And Not .EOF Then
.MoveLast
.MoveFirst
While (Not .EOF)
strEmpName = rs.Fields("EmpName")
strEmpName = "'" & strEmpName & "', "
datPTODate = rs.Fields("PTODate")
strPTODate = "'" & Format(datPTODate, "mm/dd/yyyy") & "'"
lngPTOHours = rs.Fields("PTOHrs") * 60
strUpdated = "'" & Format(Now(), "mm/dd/yyyy") & "'"
strPassThruInsert = "INSERT INTO Attendance (Attendance, Employee, Work_Date, Regular_Minutes, Attendance_Type, Lock_Times, Source, Last_Updated) VALUES (NewID(), "
strPassThruInsert = strPassThruInsert & strEmpName & strPTODate & ",lngPTOHours,2,-1,0, '" & Now() & "');"
Set q = CurrentDb.CreateQueryDef("", strPassThruInsert)
CurrentDb.QueryDefs(q).Execute
.MoveNext
Wend
End If
.Close
End With
First, you not given ANY valid reason to insert with a pass-through query.
And in fact, since EACH insert will be a 100% and separate and “discrete” insert?
Well, the time for the query processor to setup, parse the syntax, and do the insert?
Well, you not achieve ANY gains in performance, and in fact it will run no faster than a plain Jane and standard insert using DAO recodsets.
As a result:
You are knowingly as a developer wasting all kinds of time pursuing this approach and this is AFTER having been given this advice on places such as UA.
And if you knowingly are wasting developer time, and seeking an approach that costs more developer time, does not speed up your insets, then you are with full knowledge seeking an approach that costs more time, and more resources than necessary.
I suppose if you are paying for this time, and don’t care, or perhaps you are doing this as a learning exercise, then fine. But as for a working solution, the approach does not make sense, takes more code and time to develop and fix, and you are doing this will full knowledge as to the increased cost and time without any benefits I can see, or think of.
So then this begs the question?
Why would you attempt a solution will FULL knowledge that costs more time, costs more effort, and does not increase performance?
You do not given any reason as to the added benefits of this approach.
The following code is far less, far more simple, will run FASTER than your given approach. And FAR better is the date conversions that you have (are WRONG), are a non-issue.
So, you have to cook up a VERY good reason as to WHY you are perusing this course of action when:
It will not run faster than using the code I post here
(In fact, such a PT query will run slower – and by A WIDE margin).
You have errors in your date formats – you have to use ISO sql server format
(But, if you use ODBC and let the record set do the translation, then ZERO formatting and ZERO re-formatting of the date and date time values you have is NOT required).
So, until such time you explain why you are willing with full knowledge to write more code, cost more time, and cook up a solution that wastes time, and will not run faster?
Then I see little if any reason to pursue your approach?, right?
So, you need to come up with a VERY good reason as to why you are insisting on this pass-through query approach, and an approach that will run NOT run faster than what I am posting here... And in fact, as I point out, the posted solution will run MUCH faster. (Not I said a wee bit faster – but MUCH faster – in fact on a multiple order (not %, but factors of times faster). I can explain WHY this is the case (better speed) if you are wondering why.
In the meantime, you are going to have to figure out how to sleep with full knowledge of pursuing a solution that is going to cost more in developer time, and not yield any benefits in terms of speed, or even maintenance and writing of such code.
This code eliminates the date error conversions you have, and WILL RUN faster than your posted code and solution:
Dim strFuturePTOSource As String
Dim rstFrom As DAO.Recordset ' from table
Dim rstTo As DAO.Recordset ' SQL server linked table.
Set rstFrom = CurrentDb.OpenRecordset("qryROBERT", dbOpenDynaset, dbSeeChanges)
Set rstTo = CurrentDb.OpenRecordset("Attendance", dbOpenDynaset, dbSeeChanges)
Do While rstFrom.EOF = False
With rstTo
.AddNew
!Attendance = NewID()
!Employee = rstFrom!EmpName
!Work_Date = rstFrom!PTODate
!Regular_Minutes = 600
!Attendance_Type = 2
!Lock_Times = -1
!Source = 0
!Last_UPdated = Now
.Update
End With
rstFrom.MoveNext
Loop
rstTo.Close
rstFrom.Close
Edit
Given that the poster HAS made a good case that a PT query is to be used?
then this code should work:
We assume that you ALREADY created a working PT query. (and it has return records set = false). I tend to create ONE PT query in Access, and then any and all places in code can re-use that PT query at well. Also if NewID() is a scalar function (t-sql), as noted, it MUST be prefixed with dbo. So, we have to use dbo.NewID()
So, the code that is "close" or that I would suggest is this:
Dim rs As DAO.Recordset
Dim strSQL As String
Dim strSQLS As String
strSQLS = "INSERT INTO Attendance (Attendance, Employee, Work_Date, Regular_Minutes, " & _
"Attendance_Type, Lock_Times, Source, Last_Updated) " & _
"VALUES (dbo.NewID(),"
Set rs = CurrentDb.OpenRecordset("qryROBERT")
With rs
Do While .EOF = False
strSQL = strSQLS
strSQL = strSQL & qu(!EmpName) & "," & quDate(!PTODate) & _
"," & quDate(Date) & "," & (!PTOHrs * 60) & ",2,-1,0," & quDate(Now())
With CurrentDb.QueryDefs("MyPassQuery")
.SQL = strSQL
.Execute
End With
.MoveNext
Loop
rs.Close
End With
In addtion to the above, I used two helper functions to format the date, as it is a pain to do this "in-line" code, so, I have qu() (for strings), and qudate() for dates.
Public Function qudate(myDate As Variant) As String
' returns a formatted string of date, ISO format, for sql sesrver.
' format is yyyy-mm-dd regardless of local date settings
If IsNull(myDate) = True Then
qudate = ""
Else
' use ISO date format
qudate = "'" & Format(myDate, "yyyy-mm-dd HH:NN:SS") & "'"
End If
End Function
And our qu() function
Function quS(vText As Variant) As String
' takes a string and surrounds it with single quotes
If IsNull(vText) = False Then
If InStr(vText, Chr(34)) > 0 Then
vText = Replace(CStr(vText), Chr(34), "'")
End If
End If
quS = "'" & vText & "'"
End Function
Edit 2
So, the steps are:
From sql studio, get a example working that inserts data. Once you have a working insert command, then take that same (and known) working command and cut + paste it into an access query - but just ensure that the query on the Access side is created and set as pass-though. Once again, test running this PT query. If the existing sql worked in SSMS, then it will work in Access 100% exactly with the SAME query. Once that is working?
so, whatever name you gave this query is what you use in place of MyPassQuery. You can give it any name you want, but you have to use the correct (same) name in your VBA code that going to use/set the sql for that PT query. So, each loop run will in fact overwrite what is in the query, and then you do the .Execute to run it.
Well, then you run your code. And on this line for testing?
.SQL = strSQL
Do this:
debug.print strSQL
.SQL = strSQL
So, as you single step though that code, the sql the VBA is creating will have to match the known working sql you had. So, if the string output has any syntax errors or does not look 100% the same as that known working sql? Well, then you have to tweak the VBA code until such time it spits out the SAME sql string that you know works.
I am working on an Access DB, which have ODBC linked SQL Server table, and I have following script to run TSQL query, as you can see I tried to include a value from Access Forms in the query, but it fails to run. The form is opened and filled with data when I execute the script. I am wondering if this is impossible or there is another way of doing it? I am new to TSQL and SQL server, here is my question. Appreicate if someone can help. Thanks a lot.
Function formtest()
Dim qryd As QueryDef
Set qryd = CurrentDb.CreateQueryDef("")
qryd.Connect = "ODBC;DSN=SQLSERVER;"
qryd.SQL = "UPDATE dbo.table1 SET firstname = [Forms]![testform]![datainput]"
qryd.ReturnsRecords = False
qryd.Execute
End Function
The SQL Server doesn't know anything about your forms. You have to send the data with the query. Something like this:
qryd.SQL = "UPDATE dbo.table1 SET firstname = '" & [Forms]![testform]![datainput] & "'"
One thing you have to be aware of though is that if there are any single quotes in your datainput it could invalidate the SQL. It could also be a security issue. Either test for single quotes and raise an error, or replace each of them with two.
The best way to do it is to use a parameterized query. This will absolutely prevent issues SQL injection and also help with performance in many cases. Unfortunately, I don't believe you can create a paramaterized query for SQL Server using DAO. You would have to convert to ADO, which is best suited for sending queries to a SQL Engine other than Jet.
To use ADO you might have to add a reference to Microsoft ActiveX Data Objects by opening the VBA code window and selecting Tools -> References -> and checking the box next to it. Then your code would look something like this:
Dim Conn1 As ADODB.Connection
Dim Cmd1 As ADODB.Command
Dim Param1 As ADODB.Parameter
Rem Create and Open Connection Object.
Set Conn1 = New ADODB.Connection
Conn1.ConnectionString = "ODBC;DSN=SQLSERVER;"
Conn1.Open
Rem Create Command Object.
Set Cmd1 = New ADODB.Command
Cmd1.ActiveConnection = Conn1
Cmd1.CommandText = "UPDATE dbo.table1 SET firstname = ?"
Rem Create Parameter Object.
Set Param1 = Cmd1.CreateParameter(, adVarChar, adParamInput, 25)
Param1.Value = [Forms]![testform]![datainput]
Cmd1.Parameters.Append Param1
Set Param1 = Nothing
Rem Open Recordset Object.
Call Cmd1.Execute
I am a Student and ASP is my subject this year. I am trying to do the Database Connectivity for the First time. It gave me this Error while i was connecting my ASP file with MSAccess.
Code:
<%
Dim objConn, strConn, objRS
Set objConn = Server.CreateObject("ADODB.Connection")
strConn = "PROVIDER=Microsoft.ACE.OLEDB.12.0;DATA SOURCE =" & _
"C:\demo.accdb"
objConn.Open strConn
Set objRS = Server.CreateObject("ADODB.Recordset")
objRS.Open "Student", objConn, 2, 2
objRS.AddNew
objRS("idnum") = Request.Form("idnum")
objRS("firstname") = Request.Form("firstname")
objRS("lastname") = Request.Form("lastname")
objRS.Update
objRS.close
%>
**The Above code Gives the Following Error:*
ADODB.Recordset error '800a0cc1'
Item cannot be found in the collection corresponding to the requested name or ordinal.
/MyWeb/choice1.asp, line 12*
.. I also tried doing this..
..
..
Dim objConn, strConn, objRS
Set objConn = Server.CreateObject("ADODB.Connection")
strConn = "DSN=Stud"
objConn.Open strConn
and it gives me the same error.
My Database name is demo.accdb
My Table name is Student.
ApplicationPool Settings for IIS is set to "true" for using Windows 32bit.
I have also installed OLEDB ACE 12.
Please help as am totally in mess.. All I want is to insert a record in an Access Database.
Help would be appreciated.
That error has nothing to do with your connection, setup, IIS settings, or anything esoteric, and everything to do with what columns exist (or rather, don't exist) in the recordset you're opening.
What is in line 12 of your code? (In the snippet you've posted, line 12 is the "lastname" field, but I don't know if that's true for your actual code.) Check the setup of the Student table: did you spell that column name correctly? If the table column is LastN, then your code should have objRS("LastN") = Request.Form("LastName")1, not objRS("LastName").... Thankfully, neither VBScript nor SQL are case-sensitive, so you don't need to be anal, but you do need to spell things correctly.
Note that it may help you "see" what you're doing better if you write an explicit SELECT statement to return just the columns (and rows) you want, instead of opening the entire table. Also, when you're working with actual databases (which tend to have many thousands or even millions of records, rather than the half a dozen you probably have in your test database), opening entire tables is A Very Bad Idea. Well, unless you like timeout errors.
objRS.Open "SELECT TOP 0 id, firstname, lastname FROM Student", objConn, 2, 2
(Since all you're doing is adding a row, you don't actually need to return any records; hence the TOP 0.2)
1 All you "OMG! Your code is vulnerable to SQL injection!!1!" types can insert your customary rant here.
2 It's been a while since I've worked with Access; if it chokes on TOP 0 with no ORDER BY clause, try SELECT ... WHERE 1 = 2.
Using classic asp, I am trying to query a SQL Server database like so:
strSQL = "select column_1, column_2, column_3, column_4 from someview " &_
"where RecordNo=" & i
set rs=conn.Execute(strSQL)
if not rs.eof then
A = rs("column_1")
B = rs("column_2")
C = rs("column_3")
D = rs("column_4")
end if
Column_3 is an NText type, the other columns are varchar or int (sometimes there may be more than 4 columns returned) but the query only returns 1 record because of the where clause.
On the ASP page the results vary - sometimes A,B,D are populated, sometimes not - but all columns in the view contain data (when I query the SQL Server I see the expected results - all columns do contain data). If I remove column_3 which is NText from the strSQL everything works fine.
I've seen this behaviour on a couple other pages in the past. If I modify the ASP to get column_3 separately:
strSQL = "select column_3 from someview where RecordNo=" & i
The NText data is returned correctly.
Is there a maximum record length to a SQL Server recordset returned to classic ASP? Apart from splitting out the NTEXT into a separate query, is there anything else I can do?
EDIT: It just occured to me to try changing the connection string - inspired by this comment on a similar problem - the connection is via SQL Server ODBC Driver (Driver={SQL Server};).
I have had this problem. Microsoft acknowledge it somewhere on their website.
If you put the NText column last in the SELECT list, you will be able to access it ok.
However, your code cannot access any other columns after it has read the NText value. Once you move to the next row of the recordset you're OK again.
Best solution is to change your connection string though, and use something more modern. That solves the problem in the best way!
To avoid using the recordset, try this:
For 1 record returned:
arr = rs.Getrows
if IsArray(arr) then
A = arr(0)
B = arr(1)
C = arr(2)
D = arr(3)
end if
For more records:
aryList = rec.GetRows
iCount = Ubound(aryList,2)
For i = 0 to iCount
A = aryList(0,i)
B = aryList(1,i)
C = aryList(2,i)
D = aryList(3,i)
' Do something with A,B,C,D
Next
casting ntext to varchar will do the job.
You're mixing unicode data (the ntext column) with non-unicode (varchar). That may be the reason, since the ASP page has to decide which to use.
Try and use either one or the other (casting non-unicode data to unicode may be the better option).
One extra tip for those who are working with older code:
When a recordset's column value is blank using ADO/ASP and you have a single line of data, you can bypass this problem by using a parameterised Command statement and returning the string value into a variable:
Some hand-typed code hopefully explains what I mean:
' DB connection
Set objCon = Server.CreateObject("ADODB.Connection")
objCon.CursorLocation = adUseClient
objCon.Open pubDbConnString
' statement construction
Set cmd = Server.CreateObject("ADODB.Command")
Set cmd.ActiveConnection = objCon
cmd.CommandText = "SELECT ?=T.Column1, ?=T.Column2 From Table T WHERE ID=?"
cmd.CommandType = adCmdText
' add parameters
cmd.Parameters.Append cmd.CreateParameter("#column1Data", adVarChar, adParamOutput, 8000)
cmd.Parameters.Append cmd.CreateParameter("#column2Data", adTinyInt, adParamOutput)
cmd.Parameters.Append cmd.CreateParameter("#id", adBigInt, adParamInput)
cmd.Parameters("#id").value = 1
set objRS = cmd.Execute
#column1Data will contain the large string. objRS will actually not have any records in it, so be mindful of this.
In theory, this should also work with named parameters with the same results, but I have not tested this.