SQL / Excel Query Parameter with a JOIN - sql

I am learning how to use parameters in an excel-driven SQL query (in fact I am still learning SQL in general). Thanks to the nice people that helped me build my query to modify the results as I need, I want to take this a step further and supply a parameter in Excel to filter the results.
Here is my query:
SELECT
fun.FUNCTION_ID
,COALESCE(fun.parent_function, fun2.function_id) as PARENT_FUNCTION
,fun.MODULE_ID
,fun.DESCRIPTION
,fun.FUNCTION_PURPOSE
,fun.PB_OBJECT
,sec.GROUP_ID
,sec.ACCESS_LEVEL
from
MODULE_FUNCTION fun
LEFT JOIN MODULE_FUNCTION fun2
ON fun.function_id = fun2.function_id
AND fun2.function_id IN (SELECT parent_function FROM MODULE_FUNCTION)
LEFT OUTER JOIN FUNCTION_SECURITY sec
ON fun.FUNCTION_ID = sec.FUNCTION_ID
AND sec.GROUP_ID = 'GROUP_NAME'
What I need to do is allow people from a team to run this query in the excel sheet and supply their group name for the "GROUP_NAME" in the second JOIN. Unfortunately I cannot use the syntax WHERE (sec.GROUP_ID = ?) (found here) as I need to pull all results from the MODULE_FUNCTION table and only insert results on the right from the FUNCTION_SECURITY table when there is a match on the supplied group (leaving null when there is no match).
When I try to use AND (sec.GROUP_ID = ?) at the end I get a "Invalid Parameter Number" in Excel. From what I have gathered, the "?" can only be use with WHERE (and works find for me in test queries).
I have tried many things, including declaring a #parameter, but no avail.
I'm tempted to try this technique but I'd like to avoid VB if possible.

I know you said you want to avoid VB, but it isn't too complicated for what you want to do.
You can have the sheet have a cell for the group name, then a button that calls a macro where you would change the sql query to adjust for the group_id.
Something like:
Dim sql As String
sql = "select ... from ... and sec.GROUP_ID = '?'"
sql = Replace(sql, "?", Worksheets("Analysis").Range("A1").Value)
With ActiveWorkbook.Connections("connection name").OLEDBConnection
.CommandText = sql
.Refresh
End With
Where:
Worksheets("Analysis").Range("A1").Value
is the Group_ID. You can set this to a specific cell in any sheet in your workbook. I would create a button right next to it called "Refresh table" or something like that.
If you already made a table that links to a database, then there is a connection object in Excel. Go to the Data tab, then click "Connections". A new window will pop up. Find the connection that matches to the SQL query. Click on that connection and click "Properties" then change the connection name to something easy (it's usually some long name based on the server/table you connect to). Use that for the
ActiveWorkbook.Connections("connection name")
section.
Link to create button on worksheet and link to macro:
http://office.microsoft.com/en-us/excel-help/add-a-button-and-assign-a-macro-to-it-in-a-worksheet-HP010236676.aspx

Related

How to unlink a query from a structured table AND deleting that query in VBA

I know that since Excel 2016 I can simply delete a query like this:
ActiveWorkbook.Queries("aaa").delete
However this will leave an "orphaned" query connection. So if I refresh the remaining structured table, I will see the error
Query xxx was not found
I know that I can avoid this by first unlinking the query like this:
ActiveSheet.ListObjects("aaa").Unlink
How can I combine these 2 codes into a macro that:
Takes the query name as input (name in the Power Query Editor list)
Remove the link to the structured table
Delete that query
I prefer a method that works in the Queries collection of ActiveWorkbook. Because ListObjects is Sheet-dependent, and sheet name may change. Something like this:
ActiveWorkbook.Queries("aaa").[something].unlink
ActiveWorkbook.Queries("aaa").delete
Regarding ListObject.Unlink method, documentation (https://learn.microsoft.com/en-us/office/vba/api/excel.listobject.unlink) suggests:
Removes the link to a Microsoft SharePoint Foundation site from a list. Returns Nothing.
Doesn't seem relevant, as you don't mention SharePoint. Perhaps the code below is what you want.
Option Explicit
Private Sub DeleteQueryAndConnection()
Dim nameOfQueryToDelete As String
nameOfQueryToDelete = "someQuery"
With ThisWorkbook
.Queries(nameOfQueryToDelete).Delete
.Connections("Query - " & nameOfQueryToDelete).Delete
End With
End Sub
I didn't get the "Query xxx was not found" error after running this code.

How to Pass Multivalued field in query to VBA function

In my project I have a table1 that has an ID and a text field. table2 has a multi-valued field multifield wherein I can select multiple values from table (via lookup).
Now, I have query1 where I want to pull the value from multifield - not multifield.value and pass it to a VBA function, like this: IDsToNames: table2.multifield. Problem is that VBA gives me this error:
The multi-valued field 'table2.multifield` is not valied in the expression IDsToNames(table2.multifield)
Now, I've tried with IDsToNames([table2].[multifield]) with the same results.
Here is my query's SQL:
SELECT table2.Title, table2.multifield, IDstoNames(table2.multifield) AS FieldNames
FROM table2;
If I remove the IDsToNames function from the SQL, then table2.multifield by itself will return the IDs like: 5, 4, 1 properly. I'm trying to fetch the second column of table1 instead of the first one that includes the IDs. So i figured I'd try passing that field to a function and perform a string split and them look them up with dlookup in a loop. But I can't get the field data into the function.
Is there a way to do this?
Is there a way to pass a multivalued field directly to a VBA function from within an SQL statement? No, regrettably.
However, there are various alternative methods that you can implement to get the list of values stored in the field. It's easy to ramble on about the pros and cons of Access multivalued fields. I'm not going to do that in detail, but it is worth stating that the primary benefit of a multivalue field is the convenience and apparent simplicity of the Access interface being able to automatically list and allow selection of multiple values of a one-to-many relationship. Trying to mimic that behavior in Access can be very awkward and cumbersome (and often impossible). Much of the implementation details for the multivalued fields are "hidden" (i.e. not well documented or are not exposed to the standard SQL or VBA programming interfaces). This includes the ability to pass the mutlivalued field to a VBA function from within an SQL statement. Regardless of how intuitive the intended behavior seems, Access will not simply pass the same concatenated list of values that it displays to another function. But there are still times when one simply wants the list of values, made accessible in a simple list. The information linked to by Gustav is useful and should be well understood for querying multivalued fields, but it still does not address other perfectly reasonable actions required for multiple values. Hopefully the following pointers are useful.
If the values are needed within a standard SQL statement, I suggest passing the primary key value(s) to a VBA function. Then have the VBA function look up the record and retrieve the multivalued-field values using DAO recordsets.
Because this will call the VBA function for every row, this can be (very) slow. It is possible to optimize the function using various techniques, like opening a static recordset object. Further details are beyond the scope of this answer.
Since you're already in code at this point and can structure VBA and queries however you want, the most efficient query will circumvent the multivalued-field itself and use standard SQL joins to get what you need. For instance, if you want to get all of the related user names, then open and enumerate the following recordset to build your list of names:
sSQL = "SELECT table2.key, table2.multifield.value, UserTable.Username " & _
" FROM UserTable INNER JOIN table2 ON UserTable.ID = table2.multifield.Value" & _
" WHERE (table2.key = [KeyParameter])"
Set qry = CurrentDb.CreateQueryDef(, sSQL)
qry.Parameters("KeyParameter") = keyPassedToFunction
Set rs = qry.OpenRecordset
If the SQL query can/will be opened as a DAO recordset in a code context and you still need to retrieve the multivalued-field as a single field, there is a way to enumerate the multivalued-field in VBA.
If the code ends up repeatedly opening and closing multiple recordsets, especially in multiple nested loops, it is likely that you could improve efficiency by restructuring the SQL using appropriate joins and changing the data processing order.
Rant: As you might notice, it is somewhat inconsistent that the underlying recordset object of an SQL statement does indeed return an object which can be enumerated in code, although the Access SQL engine refuses to pass such an object to a VBA function. The SQL engine already deals with boxing and unboxing data into the VBA variant type, so it seems reasonable that when implementing the multivalue fields, they could have had the SQL engine simply box the multivalued recordset object and passed it to a VBA function to be handled similar to the following code... so the original attempt in the question was not unreasonable.
The following code snippet illustrates that the multivalue field is returned as a DAO.Field object containing a DAO.Recordset2 object:
Dim rs as Recordset2
Set rs = CurrentDB.OpenRecordset("SELECT table2.multifield ... FROM ...")
Dim sList As String
sList = ""
If TypeOf rs![multifield] Is Recordset2 Then
Dim rsMVF As Recordset2
Set rsMVF = rs![multifield]
Dim bFirst As Boolean
bFirst = True
rsMVF.MoveFirst
Do Until rsMVF.EOF
If bFirst Then
sList = rsMVF.Fields(0)
bFirst = False
Else
sList = sList & "," & rs.Fields(0)
End If
rsMVF.MoveNext
Loop
'* DO NOT CLOSE the Recordset based on the Multivalue field.
'* Access will close it automatically.
End If
'* sList will contain comma-separated list of values

MS access check column value and change another

I have an ms access database with some yes/no columns I want to check and set the value of a third. The statement should be something like the following
if !col1 && !col2:
col3 = no
else:
col3= yes
I keep searching but don't really understand vba and can't find what I need .. Mostly a segment of an answer to something else that I cant make work. Currently trying to create it in the "module" section is that even right? Would be best if this could be done automatically as those columns are changed or maybe run once and do all rows. Please help me get on the right track, any help greatly appreciated.
Here is what I would do:
1- Create a form and add at least one command button on it. Name it cmdMyButton or cmdAnythingThatYouWant (cmd is the prefix used in examples from Microsoft for command buttons)
2- in the design view, double click the command button so to pop the code window
3- In the onClick() function, write the code that opens up a recordset for your table, loop through records and for each row, verify the value of those 2 columns and update if needed. (look at the documentation for DAO.recordset)
Let's say you have an Access table named Table1, with some Access fields:
The following Access SQL statement will update the value of col3 based on the values of col1 and col2, for every row in Table1:
UPDATE Table1
SET col3 = NOT col1 AND NOT col2
There are a number of ways to leverage this SQL statement:
You can paste it into the Query Designer in SQL view, and execute it via the Access UI
You can run it as part of an Access macro
You can execute it in VBA, using ADO or DAO
You can execute it in VBA, using the DoCmd.RunSQL method
Instead of VBA, you can use another Automation-supporting programming language
I would just create this on a calculated column in your DB table. See screen shot below:
Notice in the properties I set a formula to acquire the desired results based on the first 2 columns. The "Result type" is set to yes/no to mimic the yes/no field. Only difference is, the third column will display a -1 (True) or 0 (False). but when displaying this information on a form, you can have it display the information in a checkbox fashion. The calculated field is also ideal cause it will only update the third column when the target record is updated, not updating the whole table, very useful if the table size starts holding over 100k records.
If you want a VBA code, then you will need a trigger. I'm assuming its a button on the form, if not, you can always change the below code to match the event you want it triggered on. The below code is also a module that can be called/used for any trigger. The code is also assuming you have a primary key.
Public Sub UpdateThirdColumn(ByVal RecordPK As String) 'RecordPK is a passed variable
'that is the primary key identifier to find the
'record in the table
'Set variavble name for SQL statement, gives you one place to update instead of hunting through code
Dim strSQL As String
'Creates the SQL string in the variable assigned
strSQL = "UPDATE {YourTableNameHere} " & _
"SET {YourThirdFieldNAmeHere} = True " & _
"WHERE ((({YourFirstFieldNAmeHere}) = True AND ({YourSecondFieldNAmeHere}) = True AND ({YourPrimaryKeyFieldHere}) ='" & RecordPK & "'));"
'Executes the SQL statement, dbFailOnError will fail if problem exists in SQL statement
CurrentDb.Execute strSQL, dbFailOnError
End Sub
'Use this code if you have a button to trigger event
Private Sub YourButton_Click()
Call UpdateThirdColumn(Me.YourPrimaryKeyControlName)
End Sub
'Use the bottom 2 codes if you want the update for each check box to be checked and update
Private Sub FirstFieldName_AfterUpdate()
Call UpdateThirdColumn(Me.YourPrimaryKeyControlName)
End Sub
Private Sub SecondFieldName_AfterUpdate()
Call UpdateThirdColumn(Me.YourPrimaryKeyControlName)
End Sub
Let me know if you need more assistance or explanation and I will be glad to help.

MS Access - SQL append query behavior is erratic

I've been working on an Access database for the last couple weeks, and it's my first project with the tool. Dealing with append queries seems to have become an utter nightmare, and is incredibly frustrating. Even more so because it seems to have simply stopped working in any consistent manner overnight.
The SQL query that I have written goes thus:
PARAMETERS noteDetails LongText, noteTime DateTime, srcUserID Long;
INSERT INTO tblNotes (NOTE_DETAILS, NOTE_TIME_CREATED, NOTE_SOURCE_USER)
VALUES (noteDetails, noteTime, srcUserID)
In tblNotes:
NOTE_ID is an AutoNumber
NOTE_DETAILS is a Long Text
NOTE_TIME_CREATED is a Date/Time
NOTE_SOURCE_USER is a Number
The way that I'm running this query is through VBA:
Set qdf = CurrentDb.QueryDefs("qerApndNote")
qdf.Parameters(0).Value = txtDetails.Value
qdf.Parameters(1).Value = Now()
qdf.Parameters(2).Value = getCurrentUserID()
qdf.Execute dbFailOnError
qdf.Close
Set qdf = Nothing
' Where CurrUserID is a global long
' txtDetails.Value is a textbox's contents
' Now() is the VBA built-in function to return a date/time combo
I have attempted to run this query manually from the navigation bar, and it works fine when done in that manner.
However, running it from VBA has resulted in such things as there being no time / date inserted, sometimes a user ID is not inserted, sometimes both, sometimes even the details text is missing.
What is it that I'm missing? Is there any general advice for users of MS Access to follow that I am not? I'm aware that NOTE is a restricted word in Access, but I really don't think that should apply here, right?
Thanks in advance!
EDIT: The form that I'm passing data from is called frmNewNote, and there is a control in it named txtDetails. It's just a regular textbox. Don't really know what else to share about that.
The getCurrentUserID function is in a module, modGlobal:
Public CurrUserID As Long
Public Function getCurrentUserID() As Long
getCurrentUserID = CurrUserID
End Function
Public Function setCurrentUserID(CurrID As Long)
CurrUserID = CurrID
End Function
It's about as barebones as you can get, really. And there is never a circumstance that you'll get to the form before SetCurrentUserID has been called during your... session? There's a login form involved.
#Andre's code for logging:
0 noteDetailsText This is a note test
1 noteTimeCreated 9/6/2017 10:28:45 AM
2 srcUserID 1
As for my architecture, um, it's just the single database file right now, on the desktop. The entire function/sub is run when you click a button, btnEnter. It does some other stuff before it gets to the SQL statement bit - checks for null values and prompts user for entries if that's the case.
I just remembered something:
MS Access 2013 calling insert queries from VBA with strange errors
You have a LongText parameter. These don't really work. See also https://stackoverflow.com/a/37052403/3820271
If the entered notes will always be <= 255 characters, change the parameter to ShortText.
If the text can be longer, you'll have to use either SunKnight0's approach with a concatenated INSERT statement.
Or use a Recordset and its .AddNew method, which will be a similar amount of code to your current solution, but also be completely safe from injection or formatting issues.
You are doing way more work than you have to. All you need is:
DoCmd.RunSQL("INSERT INTO tblNotes (NOTE_DETAILS, NOTE_TIME_CREATED, NOTE_SOURCE_USER) VALUES ('" & Me.txtDetails & "',Now()," & CurrUserID & ")")
Note the change from txtDetails.Value to Me.txtDetails which is what may have been messing you up. This of course assumes the code runs in the form's context, otherwise you have to get he value of the text field using a reference to the form.
The only other thing to consider is making sure Me.txtDetails does not have any single quotes, so probably use Replace(Me.txtDetails,"'","''") instead.
That way you can also replace DoCmd.RunSQL with MsgBox to troubleshoot the exact query.

How to use form variables with ECount/OpenRecordset in MS Access

I have a module in a large Access database which attempts to count the number of unique records in a table using the ECount (Extended D-Count) function written by Allen Browne (here: http://allenbrowne.com/ser-66.html).
As he mentions in the guide to using it:
You cannot embed a reference to a form in the arguments.
For example, this will not work:
ECount("*", "Customers", "City = Forms!Customers!City")
Instead, concatenate the value into the string:
ECount("*", "Customers", "City = """ & Forms!Customers!City & """")
Unfortunately, the query being sent in to ECount (the "domain" parameter) itself calls another query which relies on a form for input. So what I end up with is, indirectly, ECount attempting to use a form variable, even though the query string itself doesn't have the form variable written in it, so I get:
Run-time Error 3061: Too few parameters. Expected 1.
At the line: "Set rs = db.OpenRecordset(strSql)"
The query being sent to OpenRecordset is: "SELECT Key FROM Report7Query WHERE (Key Is Not Null) AND (MainCategory= "Source" AND 1=1 AND ([State]='AR') ) GROUP BY Key;". This query is generated based on inputs to a form, and Report7Query calls another query called CustomQuery which (I think) relies on a form (also called CustomQuery) for its inputs.
If I just try to run Report7Query with no forms open, a dialog box comes up asking for "Forms!CustomQuery!ConsolidatedSelection.Value", which is a variable on the CustomQuery form. The same goes if I just try to open the CustomQuery query. Interestingly, if I open the CustomQuery form in Design view and then run either Report7Query or the CustomQuery query, or even the above select statement in its own query, then it all works.
I've tried numerous approaches to fixing this, such as using QueryDefs, but I can't seem to get ECount to work. Interestingly, DCount works just fine with the exact same query.