I\'ve read a lot about SQL injection, and using parameters, from sources like bobby-tables.com. However, I\'m working with a complex application in Access, that has a lot of
I have built a fairly basic query builder class to get around the mess of string concatenation and to handle the lack of named parameters. Creating a query is fairly simple.
Public Function GetQuery() As String
With New MSAccessQueryBuilder
.QueryBody = "SELECT * FROM tblEmployees"
.AddPredicate "StartDate > @StartDate OR StatusChangeDate > @StartDate"
.AddPredicate "StatusIndicator IN (@Active, @LeaveOfAbsence) OR Grade > @Grade"
.AddPredicate "Salary > @SalaryThreshhold"
.AddPredicate "Retired = @IsRetired"
.AddStringParameter "Active", "A"
.AddLongParameter "Grade", 10
.AddBooleanParameter "IsRetired", False
.AddStringParameter "LeaveOfAbsence", "L"
.AddCurrencyParameter "SalaryThreshhold", 9999.99@
.AddDateParameter "StartDate", #3/29/2018#
.QueryFooter = "ORDER BY ID ASC"
GetQuery = .ToString
End With
End Function
The output of the ToString() method looks like:
SELECT * FROM tblEmployees WHERE 1 = 1 AND (StartDate > #3/29/2018# OR StatusChangeDate > #3/29/2018#) AND (StatusIndicator IN ('A', 'L') OR Grade > 10) AND (Salary > 9999.99) AND (Retired = False) ORDER BY ID ASC;
Each predicate is wrapped in parens to handle linked AND/OR clauses, and parameters with the same name only have to be declared once. Full code is at my github and reproduced below. I also have a version for Oracle passthrough queries that uses ADODB parameters. Eventually, I'd like to wrap both in an IQueryBuilder interface.
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 'True
END
Attribute VB_Name = "MSAccessQueryBuilder"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = True
'@Folder("VBALibrary.Data")
'@Description("Provides tools to construct Microsoft Access SQL statements containing predicates and parameters.")
Option Explicit
Private Const mlngErrorNumber As Long = vbObjectError + 513
Private Const mstrClassName As String = "MSAccessQueryBuilder"
Private Const mstrParameterExistsErrorMessage As String = "A parameter with this name has already been added to the Parameters dictionary."
Private Type TSqlBuilder
QueryBody As String
QueryFooter As String
End Type
Private mobjParameters As Object
Private mobjPredicates As Collection
Private this As TSqlBuilder
' =============================================================================
' CONSTRUCTOR / DESTRUCTOR
' =============================================================================
Private Sub Class_Initialize()
Set mobjParameters = CreateObject("Scripting.Dictionary")
Set mobjPredicates = New Collection
End Sub
' =============================================================================
' PROPERTIES
' =============================================================================
'@Description("Gets or sets the query statement (SELECT, INSERT, UPDATE, DELETE), exclusive of any predicates.")
Public Property Get QueryBody() As String
QueryBody = this.QueryBody
End Property
Public Property Let QueryBody(ByVal Value As String)
this.QueryBody = Value
End Property
'@Description("Gets or sets post-predicate query statements (e.g., GROUP BY, ORDER BY).")
Public Property Get QueryFooter() As String
QueryFooter = this.QueryFooter
End Property
Public Property Let QueryFooter(ByVal Value As String)
this.QueryFooter = Value
End Property
' =============================================================================
' PUBLIC METHODS
' =============================================================================
'@Description("Maps a boolean parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("blnValue: The parameter's value.")
Public Sub AddBooleanParameter(ByVal strName As String, ByVal blnValue As Boolean)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddBooleanParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(blnValue)
End If
End Sub
' =============================================================================
'@Description("Maps a currency parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("curValue: The parameter's value.")
Public Sub AddCurrencyParameter(ByVal strName As String, ByVal curValue As Currency)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddCurrencyParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(curValue)
End If
End Sub
' =============================================================================
'@Description("Maps a date parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("dtmValue: The parameter's value.")
Public Sub AddDateParameter(ByVal strName As String, ByVal dtmValue As Date)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddDateParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, "#" & CStr(dtmValue) & "#"
End If
End Sub
' =============================================================================
'@Description("Maps a long parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("lngValue: The parameter's value.")
Public Sub AddLongParameter(ByVal strName As String, ByVal lngValue As Long)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddNumericParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, CStr(lngValue)
End If
End Sub
' =============================================================================
'@Description("Adds a predicate to the query's WHERE criteria.")
'@Param("strPredicate: The predicate text to be added.")
Public Sub AddPredicate(ByVal strPredicate As String)
mobjPredicates.Add "(" & strPredicate & ")"
End Sub
' =============================================================================
'@Description("Maps a string parameter and its value to the query builder.")
'@Param("strName: The parameter's name.")
'@Param("strValue: The parameter's value.")
Public Sub AddStringParameter(ByVal strName As String, ByVal strValue As String)
If mobjParameters.Exists(strName) Then
Err.Raise mlngErrorNumber, mstrClassName & ".AddStringParameter", mstrParameterExistsErrorMessage
Else
mobjParameters.Add strName, "'" & strValue & "'"
End If
End Sub
' =============================================================================
'@Description("Parses the query, its predicates, and any parameter values, and outputs an SQL statement.")
'@Returns("A string containing the parsed query.")
Public Function ToString() As String
Dim strPredicatesWithValues As String
Const strErrorSource As String = "QueryBuilder.ToString"
If this.QueryBody = vbNullString Then
Err.Raise mlngErrorNumber, strErrorSource, "No query body is currently defined. Unable to build valid SQL."
End If
ToString = this.QueryBody
strPredicatesWithValues = ReplaceParametersWithValues(GetPredicatesText)
EnsureParametersHaveValues strPredicatesWithValues
If Not strPredicatesWithValues = vbNullString Then
ToString = ToString & " " & strPredicatesWithValues
End If
If Not this.QueryFooter = vbNullString Then
ToString = ToString & " " & this.QueryFooter & ";"
End If
End Function
' =============================================================================
' PRIVATE METHODS
' =============================================================================
'@Description("Ensures that all parameters defined in the query have been provided a value.")
'@Param("strQueryText: The query text to verify.")
Private Sub EnsureParametersHaveValues(ByVal strQueryText As String)
Dim strUnmatchedParameter As String
Dim lngMatchedPoisition As Long
Dim lngWordEndPosition As Long
Const strProcedureName As String = "EnsureParametersHaveValues"
lngMatchedPoisition = InStr(1, strQueryText, "@", vbTextCompare)
If lngMatchedPoisition <> 0 Then
lngWordEndPosition = InStr(lngMatchedPoisition, strQueryText, Space$(1), vbTextCompare)
strUnmatchedParameter = Mid$(strQueryText, lngMatchedPoisition, lngWordEndPosition - lngMatchedPoisition)
End If
If Not strUnmatchedParameter = vbNullString Then
Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strUnmatchedParameter & " has not been provided a value."
End If
End Sub
' =============================================================================
'@Description("Combines each predicate in the predicates collection into a single string statement.")
'@Returns("A string containing the text of all predicates added to the query builder.")
Private Function GetPredicatesText() As String
Dim strPredicates As String
Dim vntPredicate As Variant
If mobjPredicates.Count > 0 Then
strPredicates = "WHERE 1 = 1"
For Each vntPredicate In mobjPredicates
strPredicates = strPredicates & " AND " & CStr(vntPredicate)
Next vntPredicate
End If
GetPredicatesText = strPredicates
End Function
' =============================================================================
'@Description("Replaces parameters in the predicates statements with their provided values.")
'@Param("strPredicates: The text of the query's predicates.")
'@Returns("A string containing the predicates text with its parameters replaces by their provided values.")
Private Function ReplaceParametersWithValues(ByVal strPredicates As String) As String
Dim vntKey As Variant
Dim strParameterName As String
Dim strParameterValue As String
Dim strPredicatesWithValues As String
Const strProcedureName As String = "ReplaceParametersWithValues"
strPredicatesWithValues = strPredicates
For Each vntKey In mobjParameters.Keys
strParameterName = CStr(vntKey)
strParameterValue = CStr(mobjParameters(vntKey))
If InStr(1, strPredicatesWithValues, "@" & strParameterName, vbTextCompare) = 0 Then
Err.Raise mlngErrorNumber, mstrClassName & "." & strProcedureName, "Parameter " & strParameterName & " was not found in the query."
Else
strPredicatesWithValues = Replace(strPredicatesWithValues, "@" & strParameterName, strParameterValue, 1, -1, vbTextCompare)
End If
Next vntKey
ReplaceParametersWithValues = strPredicatesWithValues
End Function
' =============================================================================