Access VBA, unescaped single quotes, Replace(), and null - database

Working on a script in Microsoft VBA to take a massive flat database and split it between about 20 different tables. The script consists mainly of opening a table, checking every row in the flat database to make sure it's not a duplicate, then adding the necessary fields. Repeat for every table.
The first time I ran it everything was going well until I tried to process the name O'Malley. I think it's obvious what went wrong. A quick search on Google turned up this related StackOverflow post. I took their advice and added Replace(str, "'", "''") to every field before inputting it into the new tables. Now I've run into a new problem and Google is less helpful.
Replace(null, "'", "''") causes a run-time error, and the flat database is just riddled with null values. I can add an extra line above every Replace() call to check IsNull() and if so put null into the database instead of Replace(str, "'", "''"), although I would prefer a solution that can fit into a single line if possible. Is there any more elegant way to solve this dilemma, or will I need 216 If statements in my code?
EDIT -
Another reason that I'm searching for a more elegant solution is my duplicate checking code. At the moment I have something like the following:
'Check for duplicates
'Assume student is duplicate if it shares:
' (StudentName and School) or SSN
Set rstDuplicate = CurrentDb.OpenRecordset("select * from Student where (StudentName = '" & Replace(rstFrom("Student").Value, "'", "''") & "' AND School = '" & Replace(rstFrom("School").Value, "'", "''") & "') OR SSN = '" & Replace(rstFrom("Social").Value, "'", "''") & "'")
If rstDuplicate.RecordCount = 0 Then
'Duplicate was not found
rstTo.AddNew
' Add fields to the new table
rstTo.Update
End If
Since the Replace() calls are inline with the duplicate checking, if I were to instead use If statements to check for null then I would have to either save the result to a string or update to flat database. A function that returns Replace(str, "'", "''") OR null without the need for extra variables would be ideal.

If you want to keep everything inline, you can use an immediate If function (IIf):
IIf(IsNull(rstFrom("Student").Value), " Is Null", "= " & Replace(rstFrom("Student").Value)
That will be a nightmare to read and maintain, though. You are better off writing your own function to handle the change in comparison operator as well as the apostrophe escaping:
Function CompFld(Val As Variant) As String
If IsNull(Val) Then
CompFld = " Is Null "
Else
CompFld = "= '" & Replace(Val, "'", "''") & "' "
End If
End Function
Use it as so:
Dim SQL As String
SQL = "SELECT * FROM Student " & _
"WHERE (StudentName " & CompFld(rstFrom("Student").Value) & " AND " & _
" School " & CompFld(rstFrom("School").Value) & ") " & _
" OR (SSN " & CompFld(rstFrom("Social").Value) & ") "
Set rstDuplicate = CurrentDb.OpenRecordset(SQL)
If rstDuplicate.RecordCount = 0 Then
'Duplicate was not found
rstTo.AddNew
' Add fields to the new table
rstTo.Update
End If

A terse, yet ugly little gem handed down to me from ages ago:
Replace(str & "", "'", "''")
Appending an empty string to a null value returns an empty string in VBA, and won't modify a non-empty string.

Access' database engine will accept either single or double quotes as delimiters for string values in queries. So instead of ...
SELECT * FROM Student WHERE StudentName = 'O''Malley'
... you can do this ...
SELECT * FROM Student WHERE StudentName = "O'Malley"
That would allow you to handle inputs which contain apostrophes. OTOH, if your string inputs also contain double quotes, this will break.
I suspect you may have more going on than just the apostrophe issue, but I don't understand your big picture. You seem to be opening a third DAO recordset for each record in rstFrom, to check whether a match exists in the Student table. I would use DCount() instead.
Dim strCriteria As String
strCriteria = "StudentName = ""O'Malley"" AND School = ""foo"""
If DCount("*", "Student", strCriteria) = 0 Then
'no match found --> add it '
End If

Related

What is the most elegant way to do an Excel VBA SQL query?

I'm using this code and very similar others that are working perfectly but I'm wondering if there is a more elegant way to do it:
(Extract)
Erase ArrUserMetRecCentros
Cnn.Open
CnnQry = _
"SELECT" & _
" APP_METERS_Q_Profiles.ProfileId, " & _
" Main_Profile_Info.FieldValue, " & _
" APP_METERS_T_Access.[Read], " & _
" APP_METERS_T_Access.Write " & _
"FROM (APP_METERS_Q_Profiles " & _
" INNER JOIN Main_Profile_Info ON APP_METERS_Q_Profiles.ProfileId = Main_Profile_Info.ProfileId) " & _
" INNER JOIN APP_METERS_T_Access ON APP_METERS_Q_Profiles.ProfileId = APP_METERS_T_Access.APPMETERSQProfileId " & _
"WHERE " & _
" (((Main_Profile_Info.FieldId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSQUserId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSTDataTypeId)=1) AND " & _
" ((APP_METERS_T_Access.[Read])=-1));"
Set rst = Cnn.Execute(CnnQry)
With rst
x = x + 1
Do Until rst.EOF
For i = 1 To 4
ArrUserMetRecCentros(x, i) = rst.Fields(i - 1).Value
Next i
.MoveNext
x = x + 1
Loop
ArrUserMetRecCentros(0, 0) = x - 1
End With
Cnn.Close
This connects to a MS SQL EXPRESS server and stores the requested data in an array that I'm going to use in EXCEL.
I'm not looking for you to rewrite my code (it's my job ;)) just few tips if there's any. Things like "Not to use 'Set rst= blablabla' better option is to do 'this'. Or "define all your queries in different public string variables to keep code cleaner", etc.
Thank you very much for any help received.
Gustavo.
CnnQry = _
"SELECT" & _
" APP_METERS_Q_Profiles.ProfileId, " & _
" Main_Profile_Info.FieldValue, " & _
" APP_METERS_T_Access.[Read], " & _
" APP_METERS_T_Access.Write " & _
"FROM (APP_METERS_Q_Profiles " & _
" INNER JOIN Main_Profile_Info ON APP_METERS_Q_Profiles.ProfileId = Main_Profile_Info.ProfileId) " & _
" INNER JOIN APP_METERS_T_Access ON APP_METERS_Q_Profiles.ProfileId = APP_METERS_T_Access.APPMETERSQProfileId " & _
"WHERE " & _
" (((Main_Profile_Info.FieldId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSQUserId)=1) AND " & _
" ((APP_METERS_T_Access.APPMETERSTDataTypeId)=1) AND " & _
" ((APP_METERS_T_Access.[Read])=-1));"
That doesn't belong in code. It's a gigantic string literal: you get no syntax highlighting, no validation, no intellisense; you need to track parentheses manually, and beyond 20 or so line continuations, it stops compiling and you start doing things like CnnQry = CnnQty & "...some more inline SQL..."
If it needed parameter values, they'd probably be just concatenated in - and then you'd need to care for single quotes, and poof you have a SQL Injection vulnerability that's not only a serious security issue, but also a bug, no less.
Inline SQL is an abomination, regardless of what language it's done in. The less you have, the better.
For what it's worth I make a typo every single time I try to type CnnQry: vowels are permitted, and there's nothing wrong with using sql either.
You want SQL queries on the SQL Server side, not in your code.
CREATE PROCEDURE dbo.GiveMeAGoodName
AS BEGIN
SELECT
...
FROM ...
INNER JOIN ...
INNER JOIN ...
WHERE ...
END
And now your inline SQL in VBA becomes:
Const sql As String = "dbo.GiveMeAGoodName"
And the VBA code should use a Command to get the Recordset:
Dim cmd As ADODB.Command
Set cmd = New ADODB.Command
cmd.ActiveConnection = Cnn
cmd.CommandText = sql
cmd.CommantType = adCmdStoredProc
'add parameters if needed:
'cmd.Parameters.Append cmd.CreateParameter(...)
Dim results As ADODB.Recordset
Set results = cmd.Execute
Note that with proper parameterization as above, you're letting SQL Server do its job and deal with quoting, and Little Bobby Tables can't do any harm.
Use Range.CopyFromRecordset to dump a recordset onto a Range without looping.
I can't comment due to lack of rep.
Rather than loop your recordset at the 'With' Statement; I'd just add the recordset as a whole to the spreadsheet.
Set rst = Cnn.Execute(CnnQry)
If rst.EOF Then
'Catching empty recordsets
Else
Worksheets("Today").Range("A2").CopyFromRecordset rst
End If
Cnn.Close
To advise. We use an ADODB Connection and Recordset objects. Your code looks like you do this also.
You could look at just using the array from .GetRows method of the recordset object
see
https://learn.microsoft.com/en-us/sql/ado/reference/ado-api/getrows-method-ado?view=sql-server-ver15
You can add the wheres to your join also, like customers join orders on (customers.id=orders.id AND order.value>100)

VBA - SQL Query String

I have declared variables that store the name of columns from a SQL Table as well as variables that store their corresponding "values to find".
Dim sColumn1 As String
Dim sColumn2 As String
Dim sColumn3 As String
Dim sValue1 As String
Dim sValue2 As String
Dim sValue3 As String
sColumn1 = Sheets(1).Range("A1").Value
sColumn2 = Sheets(1).Range("B1").Value
sColumn3 = Sheets(1).Range("C1").Value
sValue1 = Sheets(1).Range("A2").Value
sValue2 = Sheets(1).Range("B2").Value
sValue3 = Sheets(1).Range("C2").Value
I want to make a dynamic query like this:
StrQuery = "SELECT * FROM dbo.Table1 WHERE ('" & sColumn1 & "') LIKE ('" & sValue1 & "') AND ('" & sColumn2 & "') LIKE ('" & sValue2 & "') AND ('" & sColumn3 & "') LIKE ('" & sValue3 & "')"
This code does not generate any errors but IT DOES NOT pull any records either. I have confirmed and all the variables are being assigned the right values.
The query above works fine if I replace the Column variables for the actual column names in the SQL Table. Like this:
StrQuery = "SELECT * FROM dbo.Table1 WHERE Column1 LIKE ('" & sValue & "') AND Column2 LIKE ('" & sValue2 & "') AND Column3 LIKE ('" & sValue3 & "')"
With this string I get results without any problem but the columns will be dynamic. Users will choose from a variety of 15 columns.
Why is it that when I use the Variable it does not work even though I know the value of the variables matches exactly the names of the Columns in the SQL Table?
Am I using the wrong format in the string so that it reads the actual value stored within the variables?
Warnings above about using parameterized queries still apply but this is how you would get this to work:
StrQuery = "SELECT * FROM dbo.Table1 WHERE " & _
sColumn1 & " LIKE ('%" & sValue & "%')" & _
" AND " & sColumn2 & " LIKE ('%" & sValue2 & "%')" & _
" AND " & sColumn3 & " LIKE ('%" & sValue3 & "%')"
The answer by #TimWilliams should address the problem of not getting any result, provided the input is valid. However, as stated in the comments, the code is a bit brittle because entering malformed or otherwise inappropriate values into the fields of the sheet might produce interesting results. So, I would like to suggest a somewhat more robust approach than simply executing the SQL string.
If you are using ADO to talk to the SQL Server, you can call stored procedures on it as explained in this SO answer. Furthermore, provided you are at least on SQL Server 2008, there is the stored procedure sp_executesql. This stored procedure allows you to execute a SQL string containing parameters. The first parameter is the SQL string, the second a string containing the parameter list and the following parameters are the actual parameters for the query. This allows you to pass in the strings representing the LIKE pattern as actual string parameters. So, no matter what the values are, they cannot break the query itself.
Regarding the column names, you should at least escape them with square brackets. That is not waterproof, but it already goes a long way regarding accidentally malformed values.
The result would be something like
sqlString = "SELECT * FROM dbo.Table1 " & _
"WHERE [" & sColumn1 & "] LIKE #value1 " & _
" AND [" & sColumn2 & "] LIKE #value2 " & _
" AND [" & sColumn3 & "] LIKE #value3 "
parameterDeclarationString = "#value1 AS NVARCHAR(1000)," & _
"#value2 AS NVARCHAR(1000)," & _
"#value3 AS NVARCHAR(1000)"
Note that the max length of the parameters is just an arbitrary guess for a sensible upper limit.

Duplicate Entry Warning Message Box

I am trying to have a message box appear when a duplicate number is entered. A duplicate in this table would be if BOTH fields match, [Source] and [Voucher_Number].
[Source] is formatted as text
[Voucher_Number] is formatted as Number
Here is what my code looks like:
If (IsNull(DLookup("Source", "tblInvoiceLog", "Source ='" &
Me.Source.Value & "'"))) And _
(IsNull(DLookup("Voucher_Number", "tblInvoiceLog", "Voucher_Number ='" &
Me.Voucher_Number.Value & "'"))) Then
Else
MsgBox "Duplicate Entery!" & Chr(13) & Chr(10) & "Please Use the Next
Available Consecutive Voucher Number", vbInformation, "Required"
End If
End Sub
I am getting:
Run Time error 3464
What I would ultimately like to do, aside from solve this problem, is in the message box return the value of the field in the [Vendor_Name] for the original entry.
Thank you for any help anyone can lend
You could try casting the DLookup return values specifically as strings to ensure you are comparing apples to apples. If you still get an error, use F8 to step through it and hover over s1stLookup and s2ndLookup to see what values are assigned to the variables.
Dim s1stLookup as String
Dim s2ndLookup as String
'Specifically cast the DLookup return values as Strings
s1stLookup = CStr(DLookup("Source", "tblInvoiceLog", "Source ='" & Me.Source.Value & "'"))
s2ndLookup = CStr(DLookup("Voucher_Number", "tblInvoiceLog", "Voucher_Number ='" & Me.Voucher_Number.Value & "'"))
If (IsNull(s1stLookup)) And (IsNull(s2ndLookup)) Then
'... Presumably some code here
Else
MsgBox "Duplicate Entery!" & vbCrLF & _
"Please Use the Next Available Consecutive Voucher Number", _
vbInformation, "Required"
End If
Currently you are testing for both values separately. But
A duplicate in this table would be if BOTH fields match, [Source] and [Voucher_Number].
So you need to test if both values exist in the same record by putting both conditions with AND into the DLookup call. And since you want to get the [Vendor_Name] if it is a duplicate, you can directly lookup this field:
Dim sVendor as String
sVendor = Nz(DLookup("Vendor_Name", "tblInvoiceLog", _
"Source = '" & Me.Source & "' AND Voucher_Number = " & Me.Voucher_Number), "")
If sVendor <> "" Then
MsgBox "Duplicate entry of vendor '" & sVendor & "'."
Else
' ok, no dupe
End If
I find it's best to always use Nz() with DLookup() so I can work with strings instead of variants. An empty string means there was no result.

VB .net Run SQl query if

Im working on my first useable practice app to help people I work with run simple queries without having to know SQL.
I have a simple form, with 4 textbox entries. columns, table, condition column, condition
what I am wanting is to run the query without the where statement if the condition column is blank, or run the conditional statement if there is something there.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles CMDQuery.Click
If Columntxtbox.Text <> "" Then
If SQL.HasConection = True Then
If Conditioncolumntxtbox.Text = "" Then
SQL.runquery("Select " & Columntxtbox.Text & " from " & tabletxtbox.Text)
Else
SQL.runquery("Select " & Columntxtbox.Text & " from " & tabletxtbox.Text & " Where " & Conditioncolumntxtbox.Text & " like'%" & conditiontxtbox.Text & "%'")
If SQL.sqldataset.Tables.Count > 0 Then
DGVData.DataSource = SQL.sqldataset.Tables(0)
End If
End If
End If
End If
End Sub
what's happening is when I fill out all the fields, it works fine, when I try to enter just the column and table field, nothing happens. I'm thinking it has to do with the third If (if I change the = to <> then I get error with keyword like when column and city is filled and nothing when all are filled )
any help would be appreciated
thanks
A part from the missing spaces, you don't show the results of the query when there is no condition
If Columntxtbox.Text <> "" Then
If SQL.HasConection = True Then
If Conditioncolumntxtbox.Text = "" Then
SQL.runquery("Select " & Columntxtbox.Text & " from [" & tabletxtbox.Text & "]")
Else
SQL.runquery("Select " & Columntxtbox.Text & " from [" & tabletxtbox.Text & _
"] Where [" & Conditioncolumntxtbox.Text & "] like '%" & _
conditiontxtbox.Text & "%'")
End If
If SQL.sqldataset.Tables.Count > 0 Then
DGVData.DataSource = SQL.sqldataset.Tables(0)
End If
End If
End If
However keep in mind that it is a very bad practice to leave your user type directly the values, column names and other part of text that will be added to your sql text without checks. The user can type anything and you could find a malicious user that know about sql injection and ruin your life.
Given the context and the requirements, (a practice app) I think that you could ignore the problem for the moment. But again, in a real context scenario where data is the most important thing that your customers have, leaving holes of this magnitude is really unprofessional.

MS-SQL Server search table using assembled criteria

I have a search page that returns results according to the criteria nominated and this works ok when each criteria is OR but when I use AND it returns bad or no results. For example the search criteria might be...
A. Author = ""
B. Subject = ""
C. Keyword = ""
D. Dated = ""
Code:
SELECT *
FROM Table
WHERE Author = '" & strAuthor & "'
AND Subject = '" & strSubject & "'
AND Keyword = '" & strKeyword & "'
AND Dated = '" & strDated & "' "
Here I have used only 4 parameters whereas in fact there are quite a few more. But the example should explain the problem... to make this work I would need to be more specific such as "if A and B" or "B, C and D" but using any parameters that are blank or NULL will not work.
Now I could write in the criteria using a conditional statement like...
SELECT *
FROM Table
WHERE
if strAuthor <> "" then
Author = '" & strAuthor & "'
end if
if strSubject <> "" then
AND Subject = '" & strSubject & "'
end if
and so on, except that writing SELECT strings like this does not work, just produces errors because the select string cannot contain additional code (that has been my finding).
If the options were only a few I could write separate select strings for each combination, but there are more than 10 different criteria which entail more than 3,628,800 combinations!
Is there a solution for this?
Here you are having 2 options:
a) Create a stored procedure and have some default value for all the columns, and in case if user doesn't pass any value, the query would run with the default value for that column.
b) Impose the rule on the front end to always select a value for all the columns.
No stored procedures are needed when using this code:
SQL = "SELECT * FROM Table WHERE ID <> ''"
if len(strAuthor) > 3 then
SQL = SQL & " AND Author = '" & strAuthor & "'"
end if
if len(strSubject) > 3 then
SQL = SQL & " AND Subject LIKE '%" + Replace(strSubject, "'", "''") + "%'"
end if
if len(strKeyword) > 3 then
SQL = SQL & " AND Description LIKE '%" + Replace(strKeyword, "'", "''") + "%'"
end if
if strCatID <> "" then
SQL = SQL & " AND Category = '" & strCatID & "'"
end if
SQL = SQL & " Order By Subject ASC "
The significance of the first line where it requests ID is to serve as a generic placeholder so that all following parameters can be prefixed with "AND".
If a variable is empty it is not included, so only those specified are used.

Resources