VBA - SQL Query String - sql-server

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.

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)

INSERT or UPDATE a record in SQL from VBA

I have put together some code in VBA which exports data from an excel sheet to a Data Base, this code (below) currently just inserts the data, but I would like to insert it based on a key (the date). So if the date already exists then in the DB the record associated with that date is replaced, if the date does not already exist, then it is inserted as usual.
Current code:
Sub SendData(AB As String, CD As String, EF As String, GH As String, IJ As String, KL As Double, MN As Double, PQ As Double, RS As Double)
Dim Date As String, Entity As String, area As String, unit As String, name As String, surname As String
Dim day As Double, weekly As Double, month As Double, year As Double
Dim objConn As ADODB.Connection
Set objConn = New ADODB.Connection
objConn.ConnectionString = "Provider=SQLOLEDB;Data Source=source;Initial Catalog=Title;Integrated Security=SSPI"
objConn.Open
Set objRec = New ADODB.Recordset
Date = Format(Range("date").Value, "YYYY-MM-DD")
Entity = AB
area = CD
unit = EF
name = GH
surname = IJ
daily = KL
weekly = MN
month = PQ
year = RS
StrSQL = "insert into table_name values ('" & Date & "', '" & Entity & _
"','" & area & "','" & unit & "','" & name & "','" & surname & "'," & daily & "," & weekly & "," & month & "," & year & ")"
Set objRec = objConn.Execute(StrSQL)
objConn.Close
Set objConn = Nothing
end sub
I am a bit confused where to place the key...
Thanks
You can do something like
StrSQL = "SELECT * FROM table_name"
objRec.Open StrSQL, objConn, , adLockOptimistic
objRec.Find "[date_value] = #" & Date & "#",,,adBookmarkFirst
If objRec.EOF Then
objRec.AddNew "[date_value] = #" & Date & "#"
End If
objRec![Entity] = Entity
'repeat for all required fields
objRec.Update
objRec.Close
I'm not 100% sure there is no minor error, as I "translated" it from my own working code, but general idea and instructions should be correct. This way you leverage recordset mechanics. I'm not sure about performance, maybe somebody more advanced will criticize it for being slow, but it works for me.
Look at sql joins you can do a join for the update and then another for the insert, if you are doing an entire range, if not use something like select count(ID) from [table_name] where [date_value]='your date' and use an if statement based on the resultant recordsets recordcount or do a select [date_value] from [table_name] of the destination table at the start of the proc in a separate rst and use .find to see if it's there
For one thing, I would change that 'Date' to something like 'TheDate', or some other descriptive, but non-reserved kind of word. Also, insert is insert and update is update. The Update (not insert) should be something like this:
StrSQL = "UPDATE table_name SET values ('" & Date & "', '" & Entity & _
"','" & area & "','" & unit & "','" & name & "','" & surname & "'," & daily & "," & weekly & "," & month & "," & year & ")
WHERE " & sWhere
Or, consider using the MERGE clause.

Docmd.RunSql Run-time error '3464' - update query

I am trying to update access table using simple VBA code, however it finished with an error. I have tried various ways to solve it but without success.
Could you please help? The code is as follow:
strSQL = "UPDATE Projects " & _
"SET Projects.id_status = '" & Me.T_project_s.Value & "' " & _
"WHERE Projects.id_project = '" & Me.curr_open.Value & "';"
I have also tried:
strSQL = "UPDATE Projects " & _
"SET Projects.id_status = [" & Me.T_project_s.Value & "] " & _
"WHERE Projects.id_project = [" & Me.curr_open.Value & "];"
or
strSQL = "UPDATE [Projects] " & _
"SET [Projects].[id_status] = '" & Me.T_project_s.Value & "' " & _
"WHERE [Projects].[id_project] = '" & Me.curr_open.Value & "';"
But it asks for a data which is available in those fields.
Your suggestion helped. I started with only a text then I have changed particular variables I wanted to be read. So in the Where statement there is no need to have beside "" also '' :).
strSQL = "UPDATE [Projects] " & _
"SET [Projects].[id_status] = '" & Me.T_project_s.Value & "' " & _
"WHERE [Projects].[id_project] = " & Me.curr_open.Value & ";"
Thanks.
Once again, here is an example where parameterization (an industry best practice in SQL programming) helps beyond avoiding SQL injection. With querydef parameters you:
avoid the need of quote enclosure;
avoid string interpolation of variables;
abstract data (i.e., VBA variables) from code (i.e., SQL statement) for cleaner scripts;
(plus as OP found out with mixed types) explicitly define the data types of values to be binded;
execute the query via DAO for smoother user interface than DoCmd.RunSQL that raises warnings to users.
Temp Query
Dim qdef As QueryDef
' PREPARED STATEMENT, DEFINING PLACEHOLDERS (NO DATA)
strSQL = "PARAMETERS [project_s_param] Text(255), [curr_open_param] Long;" & _
" UPDATE [Projects]" & _
" SET [Projects].[id_status] = [project_s_param]" & _
" WHERE [Projects].[id_project] = [curr_open_param];"
' CREATE UNNAMED TEMP QUERYDEF, ASSIGNING PREPARED STATEMENT
Set qdef = CurrentDb.CreateQueryDef("", strSQL)
' BIND VBA VALUES TO PARAMETER PLACEHOLDERS
qdef![project_s_param] = Me.T_project_s.Value
qdef![curr_open_param] = Me.curr_open.Value
' EXECUTE ACTION
qdef.Execute dbFailOnError
Set qdef = Nothing
Saved Query
Even better, save entire prepared statement as a stored Access query and avoid any SQL in VBA.
SQL (save as any regular query object whose name is referenced in VBA)
PARAMETERS [project_s_param] Text(255), [curr_open_param] Long;
UPDATE [Projects]
SET [Projects].[id_status] = [project_s_param]
WHERE [Projects].[id_project] = [curr_open_param]
VBA
Dim qdef As QueryDef
' REFERENCE EXISTING QUERYDEF, ASSIGNING PREPARED STATEMENT
Set qdef = CurrentDb.QueryDefs("mySavedQuery")
' BIND VBA VALUES TO PARAMETER PLACEHOLDERS
qdef![project_s_param] = Me.T_project_s.Value
qdef![curr_open_param] = Me.curr_open.Value
' EXECUTE ACTION
qdef.Execute dbFailOnError
Set qdef = Nothing

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.

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

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

Resources