Click here to Skip to main content
Click here to Skip to main content

Excel-addin for Database Querying by User Defined Functions

By , 2 Nov 2007
 

Introduction

IMPORTANT: DBFuncs has been merged with DBSheets and has moved here!

DBFuncs is an Excel-addin for database querying by userdefined functions. This is opposed to the Excel integrated Microsoft-Query, which is integrated statically into the worksheet and has some limitations in terms of querying possibilities and flexibility of constructing parameterized queries (Microsoft-Query allows parameterized queries only in simple queries that can be displayed graphically).

Furthermore it includes the possibility for easy filling of so called "data bound controls" (ComboBoxes and Listboxes) with data from queries.

DBFuncs has been tested extensively (actually it's in production) only with Excel XP and Microsoft-SQL Server, other databases (MySQL, Oracle, PostgreSQL, DB2 and Sybase SQL Server) have just been tested with the associated Testworkbook DBFuncsTest.xls.

To use that Testworkbook you'll need the pubs database, where I have scripts available for Oracle, Sybase, DB2, PostgreSQL and MySql on my website (the Microsoft-SQL Server version can be downloaded here).

You can also download the latest version of DBFuncs (packed in 7-ZIP format) from my website, it is distributed under the GNU Public License.

There are two principal ways to query data with DBFuncs:

  1. A (fast) list-oriented way using DBListFetch.
    Here the values are entered into a rectangular list starting from the TargetRange cell (similar to Microsoft-Query, actually the QueryTables Object is used to fill the data into the Worksheet).
  2. A record-oriented way using DBRowFetch.
    Here the values are entered into several ranges given in the Parameter list TargetArray. Each of these ranges is filled in order of appearance with the results of the query.

These user-defined functions insert the queried data outside their calling cell context, which means that the target ranges can be put anywhere in the workbook (even outside of the workbook).

Additionally, some helper functions are available:

  • chainCells, which concatenates the values in the given range together by using "," as separator, thus making the creation of the select field clause easier
  • concatCells simply concatenating cells (making the "&" operator obsolete)
  • DBString, building a quoted string from an open ended parameter list given in the argument. This can also be used to easily build wildcards into the String
  • DBinClause, building an SQL in clause from an open ended parameter list given in the argument
  • DBDate, building a quoted Date string (format YYYYMMDD) from the date value given in the argument
  • MarktwertHolen, fetching Date-oriented market data into a single cell

Finally, there is a supporting tool available for building queries and placing them into DBListFetch or DBRowFetch functions (similar to Microsoft-Query).

Using the Code

DBListFetch

DBListFetch (Query, ConnectionString (optional), TargetRange,
             FormulaRange (optional), ExtendDataArea (optional),
             HeaderInfo (optional), AutoFit (optional),
             AutoFormat (optional), ShowRowNum (optional))

The select statement for querying the values is given as a text string in parameter Query. This text string can be a dynamic formula, i.e. parameters are easily given by concatenating the query together from other cells, e.g.
"select * from TestTable where TestName = "&A1

The connection string is either given in the formula, or for standard configuration can be left out and is then set globally inside the DBfuncs.xla addin (module DBfunctions, top):

Public Const ConstConnString = "provider=SQLOLEDB,Server=..."
Public Const CnnTimeout = 10

The returned list values are written into the Range denoted by TargetRange. This can be:

  • just any range, resulting data being copied beginning with the left-uppermost cell
  • a self-defined named range (of any size) as TargetRange, which resizes the named range to the output size. This named range can be defined (and set as function parameter) either before or after results have been queried

There is an additional FormulaRange that can be specified to fill "associated" formulas (can be put anywhere (even in other workbooks), though it is only allowed outside of the data area). This FormulaRange can be:

  • either a one dimensional row-like range or
  • a self-defined named range (of any size extent, columns have to include all calculated/filled down cells) , which resizes the named range to the output size. This named range can be defined (and set as function parameter) either before or after results have been queried. Watch out when giving just one cell as the named range, this won't work as it's not possible in VBA to retrieve another assigned name of a cell and a hidden name is used to store the last extent of the formula range. The workaround is to assign at least two cells (columns or rows) to that name.

It has formulas usually referring to cell-values fetched within the data area. All Formulas contained in this area are filled down to the bottom row of the TargetRange. In case the FormulaRange starts lower than the topmost row of TargetRange, then any formulas above are left untouched (e.g. enabling possibly different calculations from the rest of the data). If the FormulaRange starts above the TargetRange, then an error is given and no formulas are being refreshed down. If a FormulaRange is assigned within the data area, an error is given as well.

In case TargetRange is a named range and the FormulaRange is adjacent, the TargetRange is automatically extended to cover the FormulaRange as well. This is especially useful when using the compound TargetRange as a lookup reference (Vlookup).

The next parameter ExtendDataArea defines how DBListFetch should behave when the queried data extends or shortens:

  • 0: DBListFetch just overwrites any existing data below the current TargetRange.
  • 1: inserts cells of just the width of the TargetRange below the current TargetRange, thus preserving any existing data. However any data right to the target range is not shifted down along the inserted data. Beware in combination with a FormulaRange that the cells below the FormulaRange are not shifted along in the current version !!
  • 2: inserts whole rows below the current TargetRange, thus preserving any existing data. Data right to the target range is now shifted down along the inserted data. This option is working safely for cells below the FormulaRange.

The parameter headerInfo defines whether Field Headers should be displayed (TRUE) in the returned list or not (FALSE = Default).

The parameter AutoFit defines whether Rows and Columns should be autofitted to the data content (TRUE) or not (FALSE = Default). There is an issue with multiple autofitted target ranges below each other, here the autofitting is not predictable (due to the unpredictable nature of the calculation order), resulting in not fitted columns sometimes.

The parameter AutoFormat defines whether the first data row's format information should be autofilled down to be reflected in all rows (TRUE) or not (FALSE = Default).

The parameter ShowRowNums defines whether Row numbers should be displayed in the first column (TRUE) or not (FALSE = Default).

DBRowFetch

DBRowFetch (Query, ConnectionString (optional),
    headerInfo(optional/ contained in paramArray), TargetRange (paramArray))

For the query and the connection string the same applies as mentioned for DBListFetch.
The value targets are given in an open ended parameter array after the query, the connection string and an optional headerInfo parameter. These parameter arguments contain ranges (either single cells or larger ranges) that are filled sequentially in order of appearance with the result of the query.
For example:

DBRowFetch("select job_desc, min_lvl, max_lvl, job_id from jobs " &_
           "where job_id = 1",,A1,A8:A9,C8:D8)

This would insert the first returned field (job_desc) of the given query into A1, then min_lvl, max_lvl into A8 and A9 and finally job_id into C8.

The optional headerInfo parameter (after the query and the connection string) defines, whether field headers should be filled into the target areas before data is being filled.
For example:

DBRowFetch("select job_desc, min_lvl, max_lvl, job_id from jobs",_
           ,TRUE,B8:E8, B9:E20)

This would insert the headers (job_desc, min_lvl, max_lvl, job_id) of the given query into B8:E8, then the data into B9:E20, row by row.

The orientation of the filled rows is always determined by the first range within the TargetRange parameter array: if this range has more columns than rows, data is filled by rows, else data is filled by columns.
For example:

DBRowFetch("select job_desc, min_lvl, max_lvl, job_id from jobs",_
           ,TRUE,A5:A8, B5:I8)

This would fill the same data as above (including a header), however column-wise. Typically this first range is used as a header range in conjunction with the headerInfo parameter.

Beware that filling of data is much slower than with DBlistFetch, so use DBRowFetch only with smaller data-sets.

chainCells(Range)

chainCells(ParameterList)

chainCells "chains" the values in the given range together by using "," as separator. Its use is mainly to facilitate the creation of the select field clause in the Query parameter, e.g.

DBRowFetch("select " & chainCells(E1:E4) & " from jobs where job_id = 1",_
           ,A1,A8:A9,C8:D8)

Where cells E1:E4 contain job_desc, min_lvl, max_lvl, job_id respectively.

concatCells

concatCells(ParameterList)

concatCells concatenates the values in the given range together. Its use is mainly to facilitate the building of very long and complex queries:

DBRowFetch(concatCells(E1:E4),,A1,A8:A9,C8:D8)

Here, cells E1:E4 contain the constituents of the query respectively.

Both chainCells and concatCells work with matrix conditionals, i.e. matrix functions of the form: {=chainCells(IF(C2:C65535="Value";A2:A65535;""))} that only chain/concat values from column A if the respective cell in column C contains Value.

DBinClause

DBinClause(ParameterList)

Creates an in clause from cell values, strings are created with quotation marks, dates are created with DBDate (see there for details, formatting is 0).

DBinClause("ABC", 1, DateRange)

The above would return "('ABC',1,'20070115')", if DateRange contained 15/01/2007 as a date value.

DBString

DBString(ParameterList)

This builds a Database compliant string (quoted) from the open ended parameter list given in the argument. This can also be used to easily build wildcards into the String, like:

DBString("_",E1,"%")

When E1 contains "test", this results in '_test%', thus matching in a like clause the strings 'stestString', 'atestAnotherString', etc.

DBDate

DBDate(DateValue, formatting (optional))

This builds from the date/datetime/time value given in the argument based on parameter formatting either

  1. (default formatting = 0) A simple datestring (format 'YYYYMMDD'), datetime values are converted to 'YYYYMMDD HH:MM:SS' and time values are converted to 'HH:MM:SS'.
  2. (formatting = 1) An ANSI compliant Date string (format date 'YYYY-MM-DD'), datetime values are converted to timestamp 'YYYY-MM-DD HH:MM:SS' and time values are converted to time time 'HH:MM:SS'.
  3. (formatting = 2) An ODBC compliant Date string (format {d 'YYYY-MM-DD'}), datetime values are converted to {ts 'YYYY-MM-DD HH:MM:SS'} and time values are converted to {t 'HH:MM:SS'}.

An example is given below:

DBDate(E1)
  • When E1 contains the excel native date 18/04/2005, this results in : '20050418' (ANSI: date '2005-04-18', ODBC: {d '2005-04-18'}).
  • When E1 contains the excel native date/time value 10/01/2004 08:05, this results in: '20040110 08:05:00' (ANSI: timestamp '2004-01-10 08:05:00', ODBC: {ts '2004-01-10 08:05:00'})
  • When E1 contains the excel native time value 08:05:05, this results in '08:05:05' (ANSI: time '08:05:05', ODBC: {t '08:05:05'})

Of course you can also change the default setting for formatting by changing the function header in Module DBfunctions in DBfuncs.xla:

DBDate(datVal As Date, Optional formatting As Integer = 0) As String

Plookup

Plookup(inputRange, selectionRange, targetRange)

Does a pattern weighted lookup of values in range inputValues in pattern lookup area selectionRange return the values contained in found row of range targetRange (if entered as a matrix function).

In case Plookup is not entered as a matrix function, Plookup returns the first column of the matching row of targetRange. If more than one row matches, it always returns values from the first matching row.

Example:

selection range | target range
1 * 3 4 5       | 11  12  13
* 2 3 * 5       | 21  22  23
* * 3 * 5       | 31  32  33
1 2 3 4 *       | 41  42  43
1 2 3 4 5       | 51  52  53

input: 1 2 3 4 x > matches 4th row -> returns 41  42  43
input: 1 2 3 4 5 > matches 5th row -> returns 51  52  53
input: x y 3 z 5 > matches 3rd row -> returns 31  32  33
input: x 2 3 z 5 > matches both 2nd and 3rd row -> returns 21  22  23
                        because row 2 is more precise

Supporting Tool Query Builder

There is a supporting tool for building queries (similar to Microsoft Query, in fact Microsoft Query is used as a frontend to build the query) available in DBfuncSupport.xla.

The query builder is invoked by right clicking anywhere in an open Excel sheet and selecting "build DBfunc query". This starts Microsoft Query (see associated documentation on using Microsoft Query to define queries), building the query either in the Query-Assistant or in Microsoft-Query itself, the option "Back to Excel" ("Zurück an Excel" in the german version) lets you insert the query into the active worksheet as one of the following "DB items":

  • a DBListFetch function
  • a DBRowFetch function
  • a Dropdown Database form
  • a Listbox Database form

The following dialog is used to achieve this:

First the DB item to be inserted has to be chosen by selecting in the "DB function/ DB bound control" choice box. Then, depending on the above choice, the target cell for the function formula, resp. the target cells for the parameters of the function can be selected:

  • Function Target: The cell where the DB function (either DBRowFetch or DBListFetch) is being placed. Available only for DBRowFetch and DBListFetch.

  • Data Target: For DB functions, the cell(s) where retrieved database data is going to be placed. For DB bound controls this corresponds to the LinkedCell property, which is the target cell where the chosen control value is put. Available for all DB items.

  • Query Target: The cell where the query is placed in case it is bigger than 255 characters. Available only for DBRowFetch and DBListFetch. If this is explicitly set then the query is always placed there, regardless of it's size! Also the query is placed there if it contains quotation marks (").

  • ConnDef Target: The cell where the connection definition is placed in case "use custom database setting" is chosen (used to override the standard connection definition in DBfuncs.xla, see DBListFetch function description above) and the connection definition string is bigger than 255 characters. Available only for DBRowFetch and DBListFetch.

  • Range Calc: The range where formulas that should be filled in along with data are going to be placed. Available only for DBListFetch.

Other possible choices are:

  • additional Data choice: The way additional Data should be treated in DBListFetch (see explanation there). Available only for DBListFetch.
  • "include Header Info?": should Field Headers be included in DBListFetch (see explanation there). Available for DBListFetch DBDropDown and DBListbox.
  • "automatic Column Fit?": should columns be autofitted for DBListFetch (see explanation there). Available only for DBListFetch.
  • "automatic Format fill?": should 1st row formats be autofilled down for DBListFetch (see explanation there). Available only for DBListFetch.
  • "show row numbers?": should row numbers be displayed in 1st column for DBListFetch? (see explanation there). Available only for DBListFetch.
  • custom database setting: if a different database from the global standard connection definition (see DBListFetch function) is used, then this is activated. Available for all DB items. Also the ODBC provider MSDASQL.1 is inserted automatically in front of the rest of the connection string.

For DB bound controls the Data target corresponds to the LinkedCell of the control. Actually only data target, "include Header Info?" and "use custom database setting?" are available, as the query and the (possible custom) connection definition are always put to the right of the data target (LinkedCell).

With DB bound controls, also a "header" label (gray background, bars as separators) is added. This is also the reason why the font in the listbox/dropdown is always a fixed width, to make calculation and alignment of header/date widths possible:

After creating the DB bound control both a question whether all currently existing DB controls within the workbook should be filled now and a hint to fill in the auto_open procedure ("Don't forget to add 'Application.Run ...' commands (in clipboard) to your workbook_open procedure (or create one)") are displayed. A minimum auto_open procedure is added to the clipboard which can be pasted into the workbook's ThisWorkbook module (this uses code originally written by Terry Kreft).

Global Connection Definition and Query Builder

There are two possibilities of connection strings: ODBC or OLEDB. ODBC has the advantage to seamlessly work with Microsoft-Query, native OLEDB is said to be faster and more reliable (there is also a generic OLEDB over ODBC by Microsoft, which emulates OLEDB if you have just a native ODBC driver available).

Now, if using ODBC connection strings (those containing "Driver="), there is a straightforward way to redefine queries directly from the cell containing the DB function: just right click on the function cell and select "build DBfunc query". Then Microsoft-query will allow you to redefine the query which you can use to overwrite the function's query.

If using OLEDB connection strings, Microsoft-query will try to connect using a system DSN named like the database as identified after the DBidentifierCCS given in the global constant connection definition:

Public Const ConstConnString = "Provider=OraOLEDB.Oracle;.. ..;User ID=pubs"
Public Const DBidentifierCCS = "User ID="
Public Const DBidentifierODBC = "UID="

The DBidentifierCCS is used to identify the database within the global constant connection definition, The DBidentifierODBC is used to identify the database within the connection definition returned by Microsoft-Query (to compare and possibly allow to insert a custom connection definition within the DB function/control). Usually these identifiers are called "Database=" (all SQLservers, MySQL), "location=" (PostgreSQL), "User ID/UID" (oracle), "Data source=" (DB2)

DB Bound Controls

Creating DB Bound Controls

DB bound controls can be created using the supporting tool described above, but can also be created manually following conventions:

  • The name property of the control must start with "DB_", required for the filling procedure (see below)
  • The LinkedCell property must be filled, this is needed to locate the associated query (right to the linked cell)
  • Correspondingly, the cell containing the query must be exactly one cell right to the LinkedCell
  • The optional connection definition string must be exactly two cells right to the LinkedCell
  • The ColumnCount property should reflect the expected column count from the query.

Filling DB Bound Controls

The DB bound controls are filled by calling the Macro initDBforms in the DBfuncSupport.xla addin the following way (preferably in an Workbook_open event proc):

Private Sub Workbook_Open()
 Application.Run "DBFuncs.xla!initDBforms", ThisWorkbook.Name
End Sub

This walks through the supplied workbooks sheets, refreshing all contained controls having names starting with "DB_" with the queries contained in the cell directly right to the LinkedCell. Additionally it checks whether the next cell right to the query cell contains something other than the empty string and in that case takes the content as a custom connection definition.

The DB bound control retains the selection during a refresh (e.g. saving/reopening the workbook). Also the height and width of the DB bound control are maintained.

Installation

Installation is simply done by copying the two Excel addins DBFuncs.xla and DBFuncSupport.xla into an XLStart folder of your choice (there are basically three of them:

  • one in C:\Programs\Microsoft Office\Office<X>\XLStart
  • one in C:\Documents and Settings\<username>\Application Data\Microsoft\Excel\XLStart
  • and the last in the Alternative start folder you specified in Options/General).

Then you'd want to adapt the standard connection string that is globally applied if no function-specific connection string is given. This is done in DBFuncs.xla, module DBfunctions, on top:

Public Const ConstConnString = "provider=SQLOLEDB,Server=…. "
Public Const CnnTimeout = 10

When starting the Testworkbook, after waiting for the – probable – connection error, you have to change the connection string(s) to suit your needs (see below for explanations).

The connection information in DBFuncsTest.xls is stored to the right of the black line, the actual connection is then selected by choosing the appropriate shortname (dropdown) in the yellow input field. After the connection has been changed, don't forget to refresh the queries/DBforms by right clicking and selecting "refresh data".

Points of Interest

The basic principle behind returning results into an area external to the Database query functions, is the utilisation of the calculation event (as mentioned in and inspired by the excelmvf project, see here for further details), as Excel won't allow ANY side-effects inside a UDF.

There is lots of information to be carried between the function call and the event (and back for status information). This is achieved by utilising a so-called calcContainer and a statusMsgContainer, basically being VBA classes abused as a simple structure that are stored into global collections called allCalcContainers and allStatusContainers. The references of the correct calcContainers and statusMsgContainers are the Workbook-name, the table name and the cell address of the calling functions which is quite a unique description of a function call (this description is called the callID in the code).

The diagram given below should clarify the process:

The real trick is to find out when resp. where to get rid of the calc containers, considering Excel's intricate way of invoking functions and the calc event handler (the above diagram is simplifying matters a bit as the chain of invocation is by no way linear in the calculations in the dependency tree).

Excel sometimes does additional calculations to take shortcuts and this makes the order of invocation basically unpredictable, so you have to take great care to just work on every function once and then remove the calcContainer (if you're interested in the actual invocations being done, set #Const DebugMe = True in both class module calcClass and module DBfunctions to see what's going on in the direct window).

After every calculation event the working calcContainers are removed, if there are no more calcContainers left, then allCalcContainers is reset to Nothing, being ready for changes in input data or function layout. Good resources for more details on the calculation order/backgrounds is Decision Model's Excel Pages, especially Calculation Secrets.

The information for resizing the list areas in DBListFetch is stored in hidden named ranges assigned to the calling function cell.

History

  • 2006: First versions using CopyFromRecordSet, Version 1.5 is the last one utilizing Range.CopyFromRecordSet for DBListFetch, as I found this function to be highly unreliable.
    From 2.0 on, I use QueryTables instead, which copies (at least until now) all data correctly into the sheet.
  • 31/01/2007: Version 2.0: Codeproject article posted
  • 03/02/2007: Version 2.1: Separate autosized naming of FormulaRange is now possible
  • 27/02/2007: Version 2.2
    • Bigfixes:
      • Check for non-range parameters in DBRowFetch's parameter list
      • chainCells & concatCells now working with general parameters
      • Remaining names from Querytables are deleted now
      • Autosizing names works now with single cells, too
  • 12/03/2007: Version 2.3
    • Bugfixes:
      • Single cell naming the TargetRange now correctly resizes to full extent
      • Autoformatting down now also works for formula cells
      • Worked around the validation list bug (calculation couldn't be set to manual in the event procedure)
      • Format filling now works correctly also with headers
      • Removed a very subtle error with multiple resizing TargetRanges beneath one another leading to DBlistFetch losing track of the upper TargetRange
      • Formatting didn't work correctly down to end of data areas
    • Enhancements:
      • a named TargetRange extends to the FormulaRange if the FormulaRange is adjacent to the TargetRange
      • Added DBinClause Function for creating bracketed in clauses from parameter arrays (ranges, etc.)
      • Included Option explicit for better compiler support
  • 25/03/2007: Version 2.4
    • Bugfixes:
      • Filtering now works correctly again
      • verketteZellen didn't work with non-ranges
    • Enhancements:
      • Added function help for all public functions
  • 09/04/2007: Version 2.5
    • Bugfixes:
      • Auto-Formatting works now for general formats (not only numerical)
      • error values (#NV!...) possible in formula range of DBListFetch
    • Enhancements:
      • Added Plookup function for weighted pattern lookup
  • 02/11/2007: DBAddin Version 1.0 now available at sourceforge

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

About the Author

rkapl
Software Developer (Senior) Austrian Federal Financing Agency
Austria Austria
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionNew functions problemmemberrichardgaspar19 Feb '13 - 2:03 
Hello!
 
I added 2 new functions in the DBFuncs addin, these calls stored procedures. Both functions is working. But I have 2 problems with them.
- When I reopen the excel which is contains these new functions. The new functions aren't running automatically and didn't refreshed the sheets. I don't know why
- When I clicked the 'refresh db' menu item, the new functions aren't called.
 
BR
Ricsi
AnswerRe: New functions problemmemberrkapl19 Feb '13 - 7:52 
Hi, I'd rather give the current Version of dbaddin a try: http://dbaddin.sourceforge.net/[^]
QuestionCall functions from UDFmemberrichardgaspar13 Feb '13 - 20:20 
Hello!
 
This post is very nice and very helpful, thank you! But I have one question, how can I call these functions an another module's UDF?
 
For example :
 
DBFunctions.DBRowFetch("EXECUTE VBA_GetCountry 10", ConnectionString, Range("B1:B10"))
 
I always got an error : Object required. Please help me.
 
BR
Richard
AnswerRe: Call functions from UDFmemberrkapl13 Feb '13 - 21:14 
First I'm Not sure whether this really works, however you would first need to create the object DBfunctions with
set DBfunctions = createobject("DBAddin.DBFunctions").
(I'm referring to the COM addin here that's available on sourceforge, you should check this...).
I'm investigating whether a direct call to the sub that actually DOES the querying/data filling is possible via VBA and what you need to set to make it work.
 
Regards,
Roland
GeneralPopulating the resulting range with the formula [modified]memberrashul15 Jul '09 - 23:33 
Hi rkapl, is it possible to insert the formula in each cell of the resulting
range . I have tried copying the formula to the other cells but the
function gets invoked that many times.I dont want to call the function
but just copy the formula from the caller cell to each cell in the resulting
range. I dont want to make use of an array formula. Any way out ?
 
Thanks and regards !
 
modified on Thursday, July 16, 2009 6:29 AM

GeneralParsing and Syntax validationmemberrashul22 Aug '08 - 3:47 
There is a lot I learned from your add-in.Great work ! Few questions to ask you. How do you parse the DBFunc functions ? I have to write a lot of user defined functions for the addin I am developing and was wondering how to parse them and check for syntax errors ?
Also is it possible to develop functions in excel which do not involve parenthesis and can be called without them ,
eg. =Func IF
 
Thanks in advance !!!

GeneralRe: Parsing and Syntax validationmemberrkapl22 Aug '08 - 9:49 
Hi rashul!
 
Thanks for your kind words, nice to hear that you can use the addin! Concerning your questions, for the first, I use following piece of code:
' functionSplit
'
' splits theString into tokens delimited by delimiter, ignoring delimiters inside quotes and brackets
' theString is split starting from startStr up to the first balancing closing Bracket (as defined by openBracket and closeBracket)
' startStr, openBracket and closeBracket are case insensitive for comparing with theString.
' returns the list of tokens, the tokens are not blank trimmed !!
Public Function functionSplit(ByVal theString As String, delimiter As String, quote As String, startStr As String, openBracket As String, closeBracket As String) As Variant
    Dim tempString As String
    Dim finalResult
 
    ' skip until we found startStr
    tempString = Mid$(theString, InStr(1, UCase$(theString), UCase$(startStr)) + Len(startStr))
    ' rip out the balancing string now...
    tempString = balancedString(tempString, openBracket, closeBracket, quote)
    If LenB(tempString) = 0 Then Err.Raise 1, , "couldn't produce balanced string from " & theString
    tempString = replaceDelimsWithSpecialSep(tempString, delimiter, quote, openBracket, closeBracket, vbTab)
    finalResult = Split(tempString, vbTab)
 
    functionSplit = finalResult
End Function
 
Sub testfunctionSplit()
    Dim check
 
    check = functionSplit("ignored, because it is before opener..,func(token3,'(', token4,internalfunc(next,next))&this is also ignored, because we have closed the bracket", ",", "'", "func", "(", ")")
    Debug.Print check(0) = "token3"
    Debug.Print check(1) = "'('"
    Debug.Print check(2) = " token4"
    Debug.Print check(3) = "internalfunc(next,next)"
 
    ' a different quote and a different delimiter:
    check = functionSplit("=func(token1;token2;""ignoredcloseBracket)""; token3;""ignored1;ignored2"");ignored1;ignored2", ";", """", "func", "(", ")")
    Debug.Print check(0) = "token1"
    Debug.Print check(1) = "token2"
    Debug.Print check(2) = """ignoredcloseBracket)"""
    Debug.Print check(3) = " token3"
    Debug.Print check(4) = """ignored1;ignored2"""
End Sub
 
' balancedString
'
' returns the minimal bracket balancing string contained in theString, opening bracket defined in openBracket, closing bracket defined in closeBracket
' regarding quoted areas inside quote (optional
Private Function balancedString(theString As String, openBracket As String, closeBracket As String, Optional quote As String = vbNullString) As String
    Dim startBalance As Long, endBalance As Long, i As Long, countOpen As Long, countClose As Long
 
    Dim quoteMode    As Boolean: quoteMode = False
    On Error GoTo err1
    startBalance = 0
    For i = 1 To Len(theString)
        If Left$(Mid$(theString, i), Len(quote)) = quote And LenB(quote) > 0 And Not quoteMode Then
            quoteMode = True
        Else
            If Not quoteMode Then
                If Left$(Mid$(theString, i), Len(openBracket)) = openBracket Then
                    If startBalance = 0 Then startBalance = i
                    countOpen = countOpen + 1
                End If
                If startBalance <> 0 And Left$(Mid$(theString, i), Len(closeBracket)) = closeBracket Then countClose = countClose + 1
            Else
                If Left$(Mid$(theString, i), Len(quote)) = quote And LenB(quote) > 0 Then quoteMode = False
            End If
        End If
 
        If countOpen = countClose And startBalance <> 0 Then
            endBalance = i - 1
            Exit For
        End If
    Next
    If endBalance = 0 Then
        balancedString = vbNullString
    Else
        balancedString = Mid$(theString, startBalance + 1, endBalance - startBalance)
    End If
End Function
 
Private Sub testBalanced()
    Debug.Print balancedString("ignored,(start,""ignore '(' , but include"",(go on, the end)),this should (all()) be excluded", "(", ")", """") = "start,""ignore '(' , but include"",(go on, the end)"
    Debug.Print balancedString("""(ignored"",(start,""ignore '(' , but include"",(go on, the end)),this should (all) be excluded", "(", ")", """") = "start,""ignore '(' , but include"",(go on, the end)"
End Sub
 
' replaceDelimsWithSpecialSep
'
' replaces the delimiter (delimiter) inside theString with specialSep, regarding both quoted areas inside quote and bracketed areas (inside openBracket/closeBracket)
Private Function replaceDelimsWithSpecialSep(theString As String, delimiter As String, quote As String, openBracket As String, closeBracket As String, specialSep As String) As String
    Dim openedBrackets As Long, quoteMode As Boolean
 
    Dim i              As Long
    For i = 1 To Len(theString)
        If Left$(Mid$(theString, i), Len(quote)) = quote And LenB(quote) > 0 And Not quoteMode Then
            quoteMode = True
        Else
            If quoteMode And Left$(Mid$(theString, i), Len(quote)) = quote And LenB(quote) > 0 Then quoteMode = False
        End If
 
        If Left$(Mid$(theString, i), Len(openBracket)) = openBracket And LenB(openBracket) > 0 And Not quoteMode Then
            openedBrackets = openedBrackets + 1
        End If
        If Left$(Mid$(theString, i), Len(closeBracket)) = closeBracket And LenB(closeBracket) > 0 And Not quoteMode Then
            openedBrackets = openedBrackets - 1
        End If
 
        If Not (openedBrackets > 0 Or quoteMode) Then
            If Left$(Mid$(theString, i), Len(delimiter)) = delimiter Then
                replaceDelimsWithSpecialSep = replaceDelimsWithSpecialSep & specialSep
            Else
                replaceDelimsWithSpecialSep = replaceDelimsWithSpecialSep & Mid$(theString, i, 1)
            End If
        Else
            replaceDelimsWithSpecialSep = replaceDelimsWithSpecialSep & Mid$(theString, i, 1)
        End If
    Next
End Function
which is used to get the individual functions arguments like follows:
functionArgs = functionSplit(activeCellFormula, ",", """", "DBLISTFETCH", "(", ")")
 
Concerning the second question: You definitely cannot write excel user defined functions that offend Excel's standard given syntax, because Excel itself parses your function before and won't let your function be invoked. What you can do however is to write a "quoted function", something like
=quote("=IF <some condition=""> THEN <dosomething> ELSE <dosomethingelse>") 
</dosomethingelse></dosomething></some>
Of course you'd be fully responsible for parsing the parts of your "quoted" function, which might become pretty complicated if you think that you'd want to include functions/operators inside your function's arguments. You'd want to have a look at http://en.wikipedia.org/wiki/Abstract_syntax_tree[^] or http://www.codeproject.com/KB/codegen/astdlrtest.aspx[^]
 
I think that sounds like a lot of fun, but I can't see the real world value in it (but still then, who cares about the real world when programming...)?
 
-regads,
Roland
GeneralRe: Parsing and Syntax validationmemberrashul22 Aug '08 - 22:20 
That is brilliant. Thanks for all your help.I will give it a shot !
GeneralRe: Parsing and Syntax validationmemberrashul24 Aug '08 - 10:33 
Could you suggest a parser that I can integrate with VBA inorder to do my parsing. Found the AST a little complicated...
Thanks a million !!!
GeneralRe: Parsing and Syntax validationmemberrkapl25 Aug '08 - 12:25 
Maybe that might help: http://vangelder.orconhosting.net.nz/excel/audxl.html[^]
 
-regrads,
Roland
GeneralRe: Parsing and Syntax validationmemberrashul25 Aug '08 - 20:30 
Really appreciate all your help !! Thank you Smile | :)
GeneralNice work!memberkevin_goff@gallup.com23 Apr '08 - 6:04 
This helps me a lot! Ty!
GeneralRe: Nice work!memberrkapl24 Apr '08 - 8:49 
Hi!
 
Nice to hear that you find the DBAddin useful. May I ask which environment you're using it in (Database, Windows/Office version)?
 
Did you download the latest Version (1.1)?
 
-regards,
Roland
GeneralNicememberPaul Conrad4 Nov '07 - 7:47 

This is a good article, like your other one on dbsheets.
 
"Real programmers just throw a bunch of 1s and 0s at the computer to see what sticks" - Pete O'Hanlon

Generalsilly question ...memberEdwin Tielen24 Oct '07 - 0:59 
Hi,
 
I am a newby with your UDF's and have reverted to this to allow myself to parameterize a query. I am stuck at the very first step: connecting to the database.
 
I am trying to connect to an Oracle data. I am trying to add a test query using the right-mouse click 'build DBFunc query' and the query works fine in the MS Query interface and get rows returned. Once I get back to Excel I see the following error:
 
DBListQuery Error in opening recordset: The connection cannot be used to perform this operation. It is either closed or invalid in this context. in query: SELECT TABLE_CONTACT.FIRST_NAME, TABLE_CONTACT.S_LAST_NAME FROM SA.TABLE_CONTACT TABLE_CONTACT WHERE (TABLE_CONTACT.S_FIRST_NAME='EDWIN'), caller: Book2!$A$3
 
Is there any possible hint you can give me to actually start using your great stuff?
 
Thanks!
 
-Edwin
 
PS please send me email at edwint_at_sgi_dot_com, as I may miss out this forum
GeneralRe: silly question ... [modified]memberrkapl24 Oct '07 - 10:57 
Hi Edwin!
 
I need to know two things: First, what connection string are you using (of course without the password) and second: Does the query work with a user having administrative rights? I had this error message in a different context (in DBSheets) using an OLEDB driver with MSSQL-Server. The OLEDB-Driver somehow requires access to the model database for a simple sp_helpdb statement (which returns all databases in the connected SQL_Server instance).
 
You can also try to dig a bit into the code to get to the possible description of the error (sorry I didn't do this in the first place, I learned lateley that the connection object has an Errors collection you can look at.) So my suggestion is to put If dbcnn.Errors.Count > 0 Then Debug.Print dbcnn.Errors.Item(0).Description right after opening the recordset in the procedure DBListQuery in module calcClass:

.
.
.
Set tableRst = New ADODB.Recordset
tableRst.CursorLocation = adUseClient
tableRst.Open query, cnn, adOpenForwardOnly, adLockReadOnly, adCmdText
If dbcnn.Errors.Count > 0 Then Debug.Print dbcnn.Errors.Item(0).Description

If Err <> 0 Then
errMsg = "DBListQuery Error in opening recordset: " & Err.Description & " in query: " & query & ", caller: " & callID
GoTo err_1
End If
retrievedRows = tableRst.RecordCount
.
.
.

 
-regards,
Roland
 

 
-- modified at 17:05 Wednesday 24th October, 2007
GeneralRe: silly question ...memberEdwin Tielen25 Oct '07 - 0:31 
Roland,
 
Thanks for your swift reply. The connect string is more or less auto-generated through the initial use of MS Query and turns out to be "Provider=MSDASQL.1;DRIVER={Microsoft ODBC for Oracle};UID=sa;PWD=;SERVER=DEVGOLD;". As I stated, I connect to the database in MS Query without problem, so I guessing that's not the problem. I have full admin privileges (being 'sa', I presume that's true). I will play around with the VBA code a bit, but have never done so, so will learn while I go.
 
-Edwin
GeneralRe: silly question ...memberEdwin Tielen25 Oct '07 - 3:14 
Roland,
 
Update from the frontlines: it started working?
 
I did exactly the same thing as I did yesterday, but now it works. It's slow, but it works. I'd suggest to not spend any more time on this and I will go ahead and build something useful around this and come back for more if I need help.
 
Thanks again. Great tool!
 
-Edwin
GeneralRe: silly question ... [modified]memberrkapl25 Oct '07 - 12:48 
Hi!
 
Well, try to use either something like "Provider=OraOLEDB.Oracle; Password=YourPwd;User ID=YourUSer"(the OLEDB Provider from Oracle) or remove the "Provider=MSDASQL.1;" part from the connection string you're using (You're actually using the OLEDB over ODBC driver from Microsoft here). Very good references on connection strings you could use are http://www.connectionstrings.com and http://www.carlprothman.net/Default.aspx?tabid=81. Anything that's either a ODBC DSNless or a OLEDB connection string will probably work (more or less) fine, there are quirks with every driver as you might find in lots of user forums. I would not recommend ODBC DSN or anything that requires installation on other machines, as this would hinder the distribution of your Workbook.
 
-hope that helps.
ROland
 

-- modified at 18:54 Thursday 25th October, 2007
GeneralExcel crashesmemberGena N10 Aug '07 - 6:37 
Hi!
 
I've tried to use the addin without any success - Excel crashes on start and disables dbfuncs for the next start. Installation was performed by putting xla files into C:\Program Files\Microsoft Office\OFFICE11\XLSTART folder. Do you have any ideas why it crashes?
 
Gena
GeneralRe: Excel crashesmemberrkapl10 Aug '07 - 11:42 
Hi Gena!
 
Sorry about that, I' haven't had any chance yet to test it with office 2003 (as seems to be your case). There are actually only two possibilities why it should crash on startup. In the Workbook_open following code is executed:
 

Private Sub Workbook_Open()
Set thecalcClass = New calcClass
On Error Resume Next
For Each theFunc In Array("DBRowFetch", "DBListFetch", "DBString", "DBDate", "DBinClause", "MarktwertHolen", "concatCells", "chainCells", "Plookup")
Application.MacroOptions Macro:=theFunc, Category:=6
Next
On Error GoTo 0
addMenus
Application.CalculateFull
End Sub

 
The first suspect is the adding of the several functions into the "Database" function category, which is not really necessary (just avoiding cluttering the userdefined category). The other suspect (but I can't really imagine that), is the adding of the two DB menus to the cell/row/column context menu (appearing at right mouse click) in procedure addMenus:
 

Sub addMenus()
Dim cont As Variant
 
On Error Resume Next
For Each cont In Array("refresh data", "build DBfunc query")
Application.CommandBars("Cell").Controls(cont).delete
Application.CommandBars("Row").Controls(cont).delete
Application.CommandBars("Column").Controls(cont).delete
Next
On Error GoTo errhandler
With Application.CommandBars("Cell").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "refresh data"
.OnAction = "resetDB"
.FaceId = 1952
End With
With Application.CommandBars("Cell").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "build DBfunc query"
.OnAction = "makeQry"
.FaceId = 2054
End With
Application.CommandBars("Cell").Controls(3).BeginGroup = True
With Application.CommandBars("Row").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "refresh data"
.OnAction = "resetDB"
.FaceId = 1952
End With
With Application.CommandBars("Row").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "build DBfunc query"
.OnAction = "makeQry"
.FaceId = 2054
End With
Application.CommandBars("Row").Controls(3).BeginGroup = True
With Application.CommandBars("Column").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "refresh data"
.OnAction = "resetDB"
.FaceId = 1952
End With
With Application.CommandBars("Column").Controls.Add(Type:=msoControlButton, Before:=1, _
Temporary:=True)
.Caption = "build DBfunc query"
.OnAction = "makeQry"
.FaceId = 2054
End With
Application.CommandBars("Column").Controls(3).BeginGroup = True
Exit Sub
errhandler:
Debug.Print "Error (" & Err.Description & ") in procedure addMenus"
Resume Next
End Sub

 
Maybe it's also related with the CalculateFull call that comes last and interferes with other addins you have installed.
 
So I'd recommend first to remove the For Each theFunc ...Next clause in the Workbook_Open procedure and see what happens.
 
If Excel still crashes then remove (uncomment) the the Application.CalculateFull call. If that still doesn't help then the addMenus is the culprit. However without the context menus you're not able to invoke the Db function builder, so that's reducing the usability of the addin quite heavily...
 
To open the DBFunc addin without having the auto_open procedure being run, use Application.EnableEvents = False in the direct window before opening the addin directly (move it from the XLSTart folder to a different one first)!
 
-hope that helps
Roland
 
BTW: If you still can't use the addin, there's going to be a new version in the next weeks I'm currently working on. It's now written as a COM Automation Addin and includes also DB queries for Word (besides other enhancements and improvements). This will however be posted as a different article.
GeneralRe: Excel crashesmemberGena N13 Aug '07 - 22:14 
Thank you for the quick response!
 
I'm a little bit busy now, will check it till the end of the week.
 

Gena
GeneralRe: Excel crashesmemberdimaomsk31 Oct '07 - 19:26 
Excel crashes when I scroll with a mouse inside ListControl like in example2 in DBFuncsTest.xls file. Same in Excel 2003 and 2007. No crashes at start.
GeneralRe: Excel crashesmemberrkapl1 Nov '07 - 10:46 
Hi!
 
Anything more detailed about the circumstances of the crash? E.g. Database you used, Query (ideally you could reproduce the crash with the pubs database for any of the databases mentioned in the Introduction). Do you mean by "scrolling" moving the scrollbar or clicking the up/down buttons of the scrollbar?
 
-regards,
Roland
GeneralNice work !memberjamat@haley.es9 Mar '07 - 9:02 
Roland, i'm very impressed whith your post, great idea !
It's very useful
Thanks for publish it.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 2 Nov 2007
Article Copyright 2007 by rkapl
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid