Access VBA with custom function in SQL - sql

I need to open a query or recordset or something (datasheet view) with some sql i build in my vba based on form control values. I have a "many to many" relationship in my data (kind of). Pretend we have a Person table, a Pet table and an ID Pet_Person table. I want to have a list of my People with a concatenated string of all their pets in one row of returned data. So....
Row Person Pets
1 Kim Lucius, Frodo, Cricket, Nemo
2 Bob Taco
And I googled and I found you can write functions in the VBA to be called from the SQL. So. Here's the problem. I have a lot of records and once opened, I cannot move around the datasheet view/query/whatever without that concatenation function being called everytime I click on a row. I only need it called once when the sql is initially ran... like a snapshot or something.
I'm not too well versed with Access and the possibilities. I've tried some things I found online that all had the same result... that concatenation function being called when I touched that resulting dataset at all.
Last thing I tried looks something like:
With db
Set qdf = .CreateQueryDef("Search Results", q)
DoCmd.OpenQuery "Search Results", , acReadOnly
.QueryDefs.Delete "Search Results"
End With
StackOverflow really never formats my stuff correctly. Probably user error.... oh, well.
Edit:
Oh Bart S. Thank you but you went away too soon for me to understand the answer if it is there. Thank you.
Oh Remou. Yes, I saw your post. I used your post. I've used many of your posts while working on this project. Why access doesn't support all SQL functions I am so used to with MySQL I have no idea. You're a great addition to this site. I should have linked to it with my question but the coffee hadn't kicked in yet.
I have my concatenation function and I am calling it within the sql. I was opening it with the docmd to open that recorset or query or whatever. But here is my issue (and I may be creating this myself by trying too many solutions at once or I might be overlooking something)... it keeps calling that function each time I touch the resulting dataset/query/thing and there's too much data for that to be happening; I am seeing the hourglass simply too much. I am positive this is because of the way I am opening it. This is intended to be the result of a search form screen thing. I'm thinking I need to just literally make another form in access and populate it with my resulting recordset. I think that is what you guys are telling me. I'm not sure. This is a weird example. But... you know with Excel, when you write an inline function of some kind to get some value for each row... and then you do a copy and paste special for just values (so not the function)... I need that. Because this function (not in Excel, obviously) must query and that takes to long to reapply each time a row is clicked on (I think it's actually requerying each row if a single row is clicked on, almost like it's rerunning the sql or something). Like the NIN/Depeche Mode song Dead Souls... It keeps calling me/it.

Here are a few thoughts and strategies for coping with the issue of constant data re-loading:
Make sure your query is set to snapshot. Same for the form.
This of course makes the data read-only, but it may help a bit.
Cache the result of your query into a local table, then show/bind that table instead of the query itself.
This will make the user wait a bit longer initially while the query is executed and saved into the local table, but it makes the interface much smoother afterwards since all data is local and doesn't need to be re-calculated.
Create a local table localPetResult (on the client side) that has all the fields matching those of the query.
Instead of binding the query itself to the datasheet form, bind the localPetResult to it, then in the form's VBA module handle the OnLoad event:
Private Sub Form_Load()
' Remove all previous data from the local cache table '
CurrentDb().Execute "DELETE FROM localPetResult"
' Define the original query '
Dim q as String
q = q & "SELECT Row, "
q = q & " Person, "
q = q & " Coalesce(""SELECT PetName FROM Pets WHERE Person='"" & [Person] & ""',"","") AS PetNames "
q = q & "FROM MyData"
' Wrap the query to insert its results into the local table '
q = "INSERT INTO localPetResult " & q
' Execute the query to cache the data '
CurrentDb().Execute q
End Sub
One you have it working, you can improve on this in many ways to make it nicer (freeze the screen and display the hourglass, dynamically bind the ersult table to the form after the data has been calculated, etc)
Cache the result of each call to the coalescing function.
I've used that to calculate the concatenation once for each record, then store the result in a Dictionary whose key is the ID of the record. Subsequent calculations for the same ID are just pulled from the Dictionary instead of re-calculated.
For instance, add the following to a VBA module. I'll assume that you use Remou's Coalesce function as well.
Option Compare Database
Option Explicit
' A Scripting.Dictionary object we'll use for caching '
Private dicCache As Object
' Call to initialise/reset the cache before/after using it '
Public Sub ResetCoalesceCache()
If Not (dicCache Is Nothing) Then
dicCache.RemoveAll
End If
End Sub
' Does the Same as Coalesce() from Remou, but attempts to '
' cache the result for faster retrieval later '
Public Function Coalesce2(key As Variant, _
sql As String, _
sep As String, _
ParamArray NameList() As Variant) As Variant
' Create the cache if it hasn't been initialised before '
If dicCache Is Nothing Then
Set dicCache = CreateObject("Scripting.Dictionary")
End If
If IsNull(key) Then
' The key is invalid, just run the normal coalesce '
Coalesce2 = Coalesce(sql, sep, NameList)
ElseIf dicCache.Exists(key) Then
' Hurray, the key exists in the cache! '
Coalesce2 = dicCache(key)
Else
' We need to calculate and then cache the data '
Coalesce2 = Coalesce(sql, sep, NameList)
dicCache.Add(key, Coalesce2)
End If
End Function
Then, to use it in your query:
' first clear the cache to make sure it doesn't contain old '
' data that could be returned by mistake '
ResetCoalesceCache
' Define the original query '
Dim q as String
q = q & "SELECT Row, "
q = q & " Person, "
q = q & " Coalesce2([Row], ""SELECT PetName FROM Pets WHERE Person='"" & [Person] & ""',"","") AS PetNames "
q = q & "FROM MyData"
' Bind to your form or whatever '
...

I always do it like this:
Dim strSql As String
strSql = "SELECT * FROM table WHERE field=something;"
Set rs = CurrentDb.OpenRecordSet(strSql)
Then use RS to perform actions. There may be better ways. You can, for example, create a query directly in Access and call it from VBA.
While looping the recordset, you can concatenate the string:
Dim strResult As String
While (Not rs.EOF)
strResult = strResult & rs!yourfield
WEnd

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

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.

Error when trying to delete data from table with LINQ to SQL

In LINQ I am trying to write a query that will, once a button is clicked, delete a record from a database table, namely "Amendments_Awaiting_Approval". This row will be found and deleted based on the value entered in a text box. So far I have this: ' Your input string
Dim parseRef As String = txtReferenceCode.Text
'Convert string to integer value '
Dim deleteRecord As Integer = Integer.Parse(parseRef)
' Query the database for the rows to be deleted. '
Dim deleteModuleChanges = _
From amendments In db.Amendments_Awaiting_Approvals _
Where amendments.Amendments_ID = deleteRecord _
Select amendments
For Each amendment As Amendments_Awaiting_Approval In deleteModuleChanges()
db.OrderDetails.DeleteOnSubmit(detail)
Next
This segment is not working. The page won't even load and when I mouse over the deleteModuleChanges() section I get the error "Overload resolution failed because no accessible ElementAtOrDefault accepts this number of arguments"
Any ideas?
deleteModuleChanges is IEnumerable, so when you add () to it VB.NET compiler tries to fire ElementAtOrDefault extension method on that collection, what can't be done without a parameter. Try that one:
For Each amendment As Amendments_Awaiting_Approval In deleteModuleChanges
db.OrderDetails.DeleteOnSubmit(detail)
Next
However, I don't know why you're iterating over amendment, but delete detail. Are you sure that's what you want to do?
And of course, you have to submit changes:
db.SubmitChanges()

Passing Excel VBA array to parental function?

I don't know if this question will make sense to begin with...
Example Given: the following value is given for a single cell (we'll call it A1): Sub-value #1|Here's another sub-value #2|Yet again, last but not least, sub-value #3. I already know someone will tell me that this is where a database should be used (trust me, my major is DB Management, I know, but I need my data in this fashion). My delimiter is the |. Now say I want to create a function that will take the LEN() of each sub-value and return the AVERAGE() of all the sub-values. If I wanted to create a single function to do this, I could use an split(), take each value, do an LEN() and return the AVERAGE().
For the example given, let's utilize cell B1. I have created similar functions in the past that would work by the following method (although not this exact one), but it requires splitting and joining the array/cell value(s) each time: =ARRAY_AVERAGE(ARRAY_LEN(A1,"|","|"),"|","|").
ARRAY_LEN(cell,delimiter[,Optional new_delimiter])
ARRAY_AVERAGE(cell,delimiter[,Optional new_delimiter])
However, I'm wondering if there might be a more dynamic approach to this. Basically, I want to split() an array with some custom VBA function, pass it to parent cell functions, and I wrap up the array by a function that will merge the array back together.
Here's how the cell function will run:
=ARRAY_AVERAGE(ARRAY_LEN(ARRAY_SPLIT(A1,"|"))).
ARRAY_SPLIT(cell,delimiter) will split the array.
ARRAY_LEN(array) will return the length of each sub-value of the array.
ARRAY_AVERAGE(array) will return the average of each sub-value of the array. Since this function returns a single value of multiple values, this will take the form of an imaginary ARRAY_JOIN(array,delimiter) that would merge the array back again.
This requires one or two additional functions in the cells, but it also lowers the number of iterations that the cell would be converting to and from a single cell value and VBA array.
What do you think? Possible? Feasible? More or less code efficient?
Now, this is a very crude example but it should give you an idea of how to get started and how you can customize this method to suit your needs. Assume you have the following data in a text file called example.txt :
Name|Age|DoB|Data1|Data2|Data3
David|25|1987-04-08|100|200|300
John|42|1960-06-21|400|500|600
Sarah|15|1997-02-01|700|800|900
This file resides in the folder C:\Downloads. To query this in VBA using ADO you'll need to reference the Microsoft ActiveX Data Objects 2.X Library where X is the latest version you have installed. I also reference the Microsoft Scripting Library to create my Schema.ini files at run-time to ensure that my data is read properly. Without the Schema.ini file you run the risk of your data not being read as you expect it to be by the driver. Numbers as text can ocassionally be read as null for no reason and dates often get returned null as well. The Schema.ini file gives the text driver an exact definition of your data and how to handle it. You don't HAVE to define every column explicitly like I have done but at the very least you should set your Format, ColNameHeader, and DateTimeFormat values.
Example Schema.ini file used:
[example.txt]
Format=Delimited(|)
ColNameHeader=True
DateTimeFormat=yyyy-mm-dd
Col1=Name Char
Col2=Age Integer
Col3=DoB Date
Col4=Data1 Integer
Col5=Data2 Integer
Col6=Data3 Integer
You'll notice that the file name is enclosed in brackets on the first line. This is NOT optional and it also allows you to define different schemas for different files. As mentioned earlier I create my Schema.ini file in VBA at run-time with something like the following:
Sub CreateSchema()
Dim fso As New FileSystemObject
Dim ts As TextStream
Set ts = fso.CreateTextFile(FILE_DIR & "Schema.ini", True)
ts.WriteLine "[example.txt]"
ts.WriteLine "Format=Delimited(|)"
ts.WriteLine "ColNameHeader=True"
ts.WriteLine "DateTimeFormat=yyyy-mm-dd"
ts.WriteLine "Col1=Name Char"
ts.WriteLine "Col2=Age Integer"
ts.WriteLine "Col3=DoB Date"
ts.WriteLine "Col4=Data1 Integer"
ts.WriteLine "Col5=Data2 Integer"
ts.WriteLine "Col6=Data3 Integer"
Set fso = Nothing
Set ts = Nothing
End Sub
You'll notice that I use the variable FILE_DIR which is a constant I define at the top of my module. Your Schema.ini file -MUST- reside in the same location as your data file. The connection string for your query also uses this directory so I define the constant to make sure they reference the same place. Here's the top of my module with the FILE_DIR constant along with the connection string and SQL query:
Option Explicit
Const FILE_DIR = "C:\Downloads\"
Const TXT_CONN = "Driver={Microsoft Text Driver (*.txt; *.csv)};Dbq=" & FILE_DIR & ";Extensions=asc,csv,tab,txt;"
Const SQL = "SELECT Name, DoB, ((Data1 + Data2 + Data3)/3) AS [Avg_of_Data]" & _
"FROM example.txt "
Notice the portion in TXT_CONN called Dbq. This is the directory where your data file(s) are stored. You'll actually define the specific file you use in the WHERE clause of your SQL string. The SQL constant contains your query string. In this case we're just selecting Name, DoB, and Averaging the three data values. With all of that out of the way you're ready to actually execute your query:
Sub QueryText()
Dim cn As New ADODB.Connection
Dim rs As New ADODB.Recordset
Dim i As Integer
'Define/open connection
With cn
.ConnectionString = TXT_CONN
.Open
'Query text file
With rs
.Open SQL, cn
.MoveFirst
'Loop through/print column names to Immediate Window
For i = 0 To .Fields.Count - 1
Debug.Print .Fields(i).Name
Next i
'Loop through recordset
While Not (.EOF Or .BOF)
'Loop through/print each column value to Immediate Window
For i = 0 To .Fields.Count - 1
Debug.Print .Fields(i)
Next i
.MoveNext
Wend
.Close 'Close recordset
End With
.Close 'Close connection to file
End With
Set rs = Nothing
Set cn = Nothing
End Sub
I know that I said doing this is extremely simple in my comments above and that this looks like a lot of work but I assure you it's not. You could use ONLY the QueryText() method and end up with similar results. However, I've included everything else to try and give you some ideas of where you can take this for your project as well as to show you how to solve problems you might run into if you're not getting the results back that you expected.
This is the guide I originally learned from: http://msdn.microsoft.com/en-us/library/ms974559.aspx
Here is a guide for doing the same thing to actual Excel files: http://support.microsoft.com/kb/257819
Lastly, here's more info on Schema.ini files: http://msdn.microsoft.com/en-us/library/windows/desktop/ms709353(v=vs.85).aspx
Hopefully you're able to find a way to make use of all this information in your line of work! A side benefit to learning all of this is that you can use ADO to query actual databases like Access, SQL Server, and Oracle. The code is nearly identical to what is printed here. Just swap out the connection string, sql string, and ignore the whole bit about a Schema.ini file.
Here are 2 example VBA UDFs that work on a single cell: enter the formula as
=AVERAGE(len_text(SPLIT_TEXT(A1,"|")))
Note that in this particular case you don't actually need the len_text function, you could use Excel's LEN() instead, but then you would have to enter the AVERAGE(..) as an array formula.
Option Explicit
Public Function Split_Text(theText As Variant, Delimiter As Variant) As Variant
Dim var As Variant
var = Split(theText, Delimiter)
Split_Text = Application.WorksheetFunction.Transpose(var)
End Function
Public Function Len_Text(something As Variant) As Variant
Dim j As Long
Dim k As Long
Dim var() As Variant
If IsObject(something) Then
something = something.Value2
End If
ReDim var(LBound(something) To UBound(something), LBound(something, 2) To UBound(something, 2))
For j = LBound(something) To UBound(something)
For k = LBound(something, 2) To UBound(something, 2)
var(j, k) = Len(something(j, k))
Next k
Next j
Len_Text = var
End Function