Microsoft Access VBA: need to get sql representation programmatically from existing query - sql

I need to be able to get the sql "property" from an existing query. So for example I could define strRS as:
strRS = Me.RecordSource '.RecordSource returns the name of a query
Then pass that value to as yet undefined function UnknownFunction(strRS) that takes strRS as a value:
strSQL = UnknownFunction(strRS)
Then the desired output, strSQL would be the original SQL string that defines the query pointed to by the form's "RecordSource" property.
The closest I've been able to get is that there may be a solution using QueryDef, but that is for queries made on the fly? What should the UnknownFunction be?
Once I have the strSQL string then I can extract from it to make a related query.

Ok, it is trivial IF one doesn't make it too complicated. I probably was getting the wrong answer at first because I was confusing "QueryDefs" with "QueryDef", which I should have known.
So without further ado, the full working code is:
Function FCN_QClip(strQRY As String) As String
qds = CurrentDb.QueryDefs(strQRY).SQL
FCN_QClip = qds
End Function
?FCN_QClip("QueryName") will yield the desired result in the immediate window for an existing query. Thanks to Nathan.
The solution may also be found at Social MSDN.

Related

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

Can we format, in VBA, a query field properties after it is create?

In Access, when creating a new query using vba, some of the data I'm getting are currency, but it comes by default as General Number. I would like to change it's format like we can do in the Property Sheet, but I can't seem to find a way.
I have tried
db.QueryDefs("TestQuery").Fields("SumOfSomething").Type = DAO.dbCurrency
But then I get: Run-time error '3219': Invalid operation.
I have searched around a bit and found a similar question (but couldn't find it back) to which the answer was that you can't change that after it is created. I fail to see the point of having the possibility to change the type if we can't actually do it.
So, in the end, can we change (in VBA) the properties of a field after the query is created?
Your question appears to be simple, but as you've probably guessed by now, it isn't.
The format property is a custom property of a field. They can be present, and if they are, they should be changed using the Field.Properties collection, but if they aren't, they need to be created using Field.CreateProperty method and then appended to the Fields.Properties collection
Const currencyFormat As String = "€ #,##0.00;€ #,##0.00-"'Dutch currency format, you need to set this for your locale
Dim qd As DAO.QueryDef
Set qd = CurrentDb.QueryDefs("TestQuery")
On Error Resume Next
qd.Fields("SumOfSomething").Properties!Format = currencyFormat
If Err.Number = 3270 Then 'Property not found, field had no format set
qd.Fields("SumOfSomething").Properties.Append qd.Fields("SumOfSomething").CreateProperty("Format", dbText, currencyFormat)
End If
On Error GoTo 0
You can get the currency format for your locale by setting a field to use it, and then using ?CurrentDb.QueryDefs("TestQuery").Fields("SumOfSomething").Properties!Format
We have to be clear here on the goal. Do you want to “just” cast or change the column type returned in a query, or do you want to modify the actual column type in the database table? These are two VERY different goals.
To use code to modify a column type, you have to create (add) the new column type, transfer the data, and then delete the old column, and then re-name the column back to the old type.
In other words, you can’t just change the data type, since Access will THEN have to update each and every row to the new data type.
The UI system in fact does the above behind the scenes.
If you use DAO code, then you need the following code outlined here:
http://accessblog.net/2007/03/how-to-change-field-type-using-dao.html
However, above is quite a bit of code. In place of DAO code to add the column, you CAN use a DDL command.
(SQL data definition language) to modify the type. Thus in code you can do this:
Dim db As dao.Database
Dim strSQL As String
Set db = CurrentDb
strSQL = "ALTER TABLE dbo_tblHotels1 ALTER COLUMN MyAmount currency"
db.Execute strSQL, dbFailOnError
So in above, we change the column "MyAmount" to currency type.

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 send a query string from function

I'm having a hard time coming up with a good title, but i hope i can explain the situation better. I currently have a query with a criteria which is ("11:00 - 21:00" Or "11:01 - 21:00"), this works perfectly fine when executed, however i will need this criteria in multiple queries therefore i decided to come up with function like below
Function timeIntervals()
timeIntervals = "11:00 - 21:00" & " Or " & "11:01 - 21:00"
End Function
and call it in each query, therefore each time i require to modify this string i can do it through that one instance, however when running this string above it does not function, im assuming its caused by the quotes on the Or, ive tried triple quotes """ and chr(34), however it doesn't work, can someone suggest a work around thank you!
As Remou indicated, you won't be able to get this to work. If you really want to do the check via a VBA function, you could write something like this:
Function timeIntervals(Value) As Boolean
If Value = "11:00 - 21:00" Or Value = "11:01 - 21:00" Then
timeIntervals = True
End If
End Function
Pass the value you want to check, and if the resulting function is true you then display the row. Something like: where timeIntervals(myvalue) = true.
Probably the best solution though is to make a table for the timeIntervals. Then in your query simply write something like:
Where MyValue IN(Select timeValue from timeIntervals)
Using this latter method you can update the table which will update the results for all users, and doesn't require a re-release of your front-end.
No matter what you do with quotes, that is not going to work, because Or will be returned as a string, not the boolean Or that you want. You could use a hidden form with two textboxes, then you can say:
WHERE timeIntervals = Forms!MyForm!Text1 Or Forms!MyForm!Text2
as long as the form remains open, your queries and any forms or reports based on them will work, furthermore, it will be very easy to change the intervals without modifying code.

MS-Access: Replace "bracket"

In one of the ms-access table I work with we have a text field with a set size.
At the end of this field there is some extra code that varies depending on the situation.
I'm looking for a way to remove one of these code but even when the last part is truncated by the field maximum size.
Let's call the field "field" and the code I'm looking to remove "abc-longcode".
If I use the replace SQL function with the string abc-longcode the query will only work when the code is complete.
If I also want my update query (that does nothing but remove this specific code at the end of my field) to work on incomplete codes how would that translate into ms-SQL?
It would have to remove (or replace with "" to be precise) all of the following (example of course, not the real codes):
abc-longcode
abc-longcod
abc-longco
abc-longc
abc-long
abc-lon
abc-lo
abc-l
Obviously I could do that with several queries. Each one replacing one of the expected truncated codes... but it doesn't sound optimal.
Also, when the field is big enough to get all of the code, there can sometime be extra details at the end that I'll also want to keep so I cannot either just look for "abc-l" and delete everything that follows :\
This query (or queries if I can't find a better way) will be held directly into the .mdb database.
So while I can think of several ways to do this outside of a ms-sql query, it doesn't help me.
Any help?
Thanks.
You can write a custom VBA replace method that will replace any of the given cases {"abc-longcode", ... "abc-l"}. This is essentially the same tack as your "several queries" idea, except it would only be one query. My VBA is rusty, but something like:
public function ReplaceCodes(str as string) as string
dim returnString as string
returnString = str
returnString = replace(returnString,"abc-longcode","")
// ... etc...
ReplaceCodes = returnString
end function
I may have gotten the parameter order wrong on replace :)
I would use my own custom function to do this using the split function to get the first part of the string. You can then use that value in the update query.
Public Function FirstPart(thetext As String) As String
Dim ret As String
Dim arrSplitText As Variant
arrSplitText = Split(thetext, "-")
ret = arrSplitText(0)
FirstPart = ret
End Function
Can you use:
Left(FieldX,InStr(FieldX,"abc-")-1)
EDIT re Comment
If there is a space or other standard delimiter:
IIf(InStr(InStr(FieldX, "abc-"), FieldX, " ") = 0, Left(FieldX, InStr(FieldX, "abc-") - 1), Replace(FieldX, Mid(FieldX, InStr(FieldX, "abc-"), InStr(InStr(FieldX, "abc-"), FieldX, " ") - InStr(FieldX, "abc-")), ""))