Showing posts with label OLEDB. Show all posts
Showing posts with label OLEDB. Show all posts

Monday, 29 October 2018

OLEDB Simple Provider C# Custom Implementation

I am delighted to be able to say that I have succeeded in getting a C# Custom Implementation of a Data Source for use with the OLEDB Simple Provider. The code below demonstrates how to access in-memory arrays via ADO Recordsets for use in VBA clients..

This posts follows on from both the OLEDB Simple Provider OSP Toolkit Documentation post and the OLEDB Simple Provider - C++ Sample Step Thru post; together these form the investigation and documentation posts. I have been playing detective with this technology and have stepped through an old C++ sample, Googling for details as to how it works and accumulating documentation links before they fade forever. These have now given fruit in this post which shows how to achieve the same in C#.

The OLEDB Simple Provider

Writing a full OLEDB Provider is very hard, I've had some problems compiling even the free ATL/OLEDB sample that comes with Visual Studio 2017. A full OLEDB Provider requires the full implementation of a great many COM interfaces. Active Template Library (ATL) goes some way to give C++ default implementations of these interfaces but nevertheless OLEDB is complicated. As OLEDB is being replaced by .NET technologies, OLEDB is not worth hugely investing in.

So it is excellent news to know that there is the Simple Provider which provides a framework which a developer can customise by implementing only two (at minimum) interfaces, DataSource and OLEDBSimpleProvider. This is like being given a base class from which to inherit and override a minimum number of methods. Timesaver. Lifesaver.

C# Assembly Source Code

The following source code is for a .NET Framework Assembly (I built against .NET Framework 4.6.1) , register for COM interop and run Visual Studio under Administrator rights so it can access the registry.

Hard coded demonstration tables.

So I wanted to focus on the two interfaces that require implementing, DataSource and OLEDBSimpleProvider. To that end the code below does not connect to a database for its data, or open a file or read from a block of cells from an Excel worksheet. Instead to keep things simple, it initializes an in-memory array which is the case below is hard coded.

Clearly, the array could be dynamic as part of your own application. It should also be obvious that you can write your own code to open a file acting as a database.

The code below is also usefully annotated with links to Microsoft Docs.

C# Project requires references to COM libraries MSDATASRC and MSDAOSP

To make the code below compile you will need to add COM references to two separate type libraries. I give the file locations here below, though they may differ for your installation. You can also find them by looking through a VBA Tools->References dialog for there descriptions also given below.

C:\Windows\SysWOW64\msdatsrc.tlb - MSDATASRC - Microsoft Data Source Interfaces for ActiveX Data Binding Type Library
C:\Windows\SysWOW64\simpdata.tlb - MSDAOSP   - Microsoft OLE DB Simple Provider 1.5 Library

Once compiled and registered (don't forget Visual Studio needs Administrator rights to register your assembly in the registry), then you can run the sample VBA code given below (page/scroll down).

using MSDAOSP;
using MSDATASRC;
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Threading;

// Added COM Reference to C:\Windows\SysWOW64\msdatsrc.tlb - MSDATASRC - Microsoft Data Source Interfaces for ActiveX Data Binding Type Library
// Added COM Reference to C:\Windows\SysWOW64\simpdata.tlb - MSDAOSP   - Microsoft OLE DB Simple Provider 1.5 Library

namespace SimpleOLEDBProvider1

{
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(MSDATASRC.DataSource))]
    [ComVisible(true)]
    public class MyDataSource : MSDATASRC.DataSource
    {
        
        List<MSDATASRC.DataSourceListener> _listListeners = new List<MSDATASRC.DataSourceListener>();
        Dictionary<string, MyCustomRecordset> _dataMembers = new Dictionary<string, MyCustomRecordset>();


        ~MyDataSource()
        {   // release all listeners
            while (_listListeners.Count > 0)
            {
                MSDATASRC.DataSourceListener head = _listListeners[0];
                head = null; // calls IUnknown::Release (hopefully)
                _listListeners.RemoveAt(0);
            }
        }

        void MSDATASRC.DataSource.addDataSourceListener(DataSourceListener pDSL)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms716931(v=vs.85)
            _listListeners.Add(pDSL);
        }

        dynamic MSDATASRC.DataSource.getDataMember(string bstrDM, ref Guid riid)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms724549(v=vs.85)

            MyCustomRecordset retval = null;

            if (_dataMembers.ContainsKey(bstrDM))
            {
                retval = _dataMembers[bstrDM];
            }
            else
            {
                retval = new MyCustomRecordset();
                retval.Initialize(bstrDM);
                _dataMembers.Add(bstrDM, retval);
            }

            return retval;
        }

        string MSDATASRC.DataSource.getDataMemberName(int lIndex)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms715947(v=vs.85)
            List<string> keys = new List<string>();
            keys.AddRange(_dataMembers.Keys);
            return keys[lIndex];
        }

        int MSDATASRC.DataSource.getDataMemberCount()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms721173(v=vs.85)
            return _dataMembers.Count;
        }

        void MSDATASRC.DataSource.removeDataSourceListener(DataSourceListener pDSL)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms724012(v=vs.85)
            _listListeners.Remove(pDSL);
        }
    }

    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(MSDAOSP.OLEDBSimpleProvider))]
    [ComVisible(true)]
    public class MyCustomRecordset : MSDAOSP.OLEDBSimpleProvider
    {
        private List<object[]> _tabularData = null;
        private string _bstrDM = null;
        private List<MSDAOSP.OLEDBSimpleProviderListener> _listListeners = new List<MSDAOSP.OLEDBSimpleProviderListener>();

        ~MyCustomRecordset()
        {
            while (_listListeners.Count > 0)
            {
                MSDAOSP.OLEDBSimpleProviderListener head = _listListeners[0];
                head = null; // calls IUnknown::Release (hopefully)
                _listListeners.RemoveAt(0);
            }
        }

        public void Initialize(string bstrDM)
        {
            //
            // we got some hard coded statrting tables for demonstration purposes only
            // in a real situation one would open a file or something
            //
            switch (bstrDM.ToLower())
            {
                case "customers":

                    _tabularData = new List<object[]>();
                    _tabularData.Add(new object[4] { "CustomerId", "CustomerName", "ContactName", "Country" });
                    _tabularData.Add(new object[4] { 1, "Big Corp", "Mandy", "USA" });
                    _tabularData.Add(new object[4] { 2, "Medium Corp", "Bob", "Canada" });
                    _tabularData.Add(new object[4] { 3, "Small Corp", "Jose", "Mexico" });

                    break;
                case "orders":

                    _tabularData = new List<object[]>();
                    _tabularData.Add(new object[3] { "OrderId", "CustomerId", "OrderDate" });
                    _tabularData.Add(new object[3] { 420, 2, new DateTime(2018, 10, 10) });
                    _tabularData.Add(new object[3] { 421, 3, new DateTime(2018, 10, 11) });
                    _tabularData.Add(new object[3] { 422, 1, new DateTime(2018, 10, 12) });
                    _tabularData.Add(new object[3] { 423, 2, new DateTime(2018, 10, 13) });

                    break;
                default:
                    // if unrecognised hand back default of colors
                    _tabularData = new List<object[]>();
                    _tabularData.Add(new object[2] { "ColorName", "ColorRGB" });
                    _tabularData.Add(new object[2] { "Red", "FF0000" });
                    _tabularData.Add(new object[2] { "Green", "00FF00" });
                    _tabularData.Add(new object[2] { "Blue", "0000FF" });
                    break;
            }

            _bstrDM = bstrDM;
        }

        void OLEDBSimpleProvider.addOLEDBSimpleProviderListener(OLEDBSimpleProviderListener pospIListener)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms713697(v=vs.85)
            _listListeners.Add(pospIListener);
        }

        int OLEDBSimpleProvider.deleteRows(int iRow, int cRows)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms713665(v=vs.85)

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.aboutToDeleteRows(iRow, cRows);

            _tabularData.RemoveRange(iRow, cRows);

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.deletedRows(iRow, cRows);

            return cRows;
        }

        int OLEDBSimpleProvider.find(int iRowStart, int iColumn, object val, OSPFIND findFlags, OSPCOMP compType)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms709764(v=vs.85)
            // not yet implemented
            return -1;
        }


        int OLEDBSimpleProvider.getColumnCount()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms715965(v=vs.85)

            object[] colHeaders = _tabularData[0];
            int totalColumnCount = colHeaders.Length;
            return totalColumnCount;
        }

        int OLEDBSimpleProvider.getEstimatedRows()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms713703(v=vs.85)
            // should not include row header

            int totalRowCount = _tabularData.Count;
            return (totalRowCount - 1);
        }

        string OLEDBSimpleProvider.getLocale()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms711216(v=vs.85)
            //return "en-us";
            var name = Thread.CurrentThread.CurrentCulture.Name;
            var locale = name.Split('-')[1];
            return locale;
        }

        int OLEDBSimpleProvider.getRowCount()
        {
            // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms715931(v=vs.85)
            // should not include row header

            int totalRowCount = _tabularData.Count;
            return (totalRowCount - 1);
        }

        OSPRW OLEDBSimpleProvider.getRWStatus(int iRow, int iColumn)
        {
            // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms709946(v=vs.85)
            return OSPRW.OSPRW_READWRITE;
        }

        dynamic OLEDBSimpleProvider.getVariant(int iRow, int iColumn, OSPFORMAT format)
        {
            // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms725341(v=vs.85)

            if (iRow < _tabularData.Count)
            {
                object[] row = _tabularData[iRow];
                return row[iColumn - 1];  // columns start at 1 it seems
            }
            else
            {
                // oddly, when a record is removed we still get called for dead row
                // so until we diagnose and fix this bug return a null
                return null;
            }
        }

        int OLEDBSimpleProvider.insertRows(int iRow, int cRows)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms721207(v=vs.85)

            object[] colHeaders = _tabularData[0];
            int totalColumnCount = colHeaders.Length;

            object[] blankRow = new object[totalColumnCount];

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.aboutToInsertRows(iRow, cRows);


            for (int i = 0; i < cRows; i++)
            {
                _tabularData.Insert(iRow, blankRow);
            }

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.insertedRows(iRow, cRows);

            return cRows;
        }

        int OLEDBSimpleProvider.isAsync()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms723042(v=vs.85)
            return 0; // not async
        }


        void OLEDBSimpleProvider.removeOLEDBSimpleProviderListener(OLEDBSimpleProviderListener pospIListener)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms712993(v=vs.85)
            _listListeners.Remove(pospIListener);
        }

        void OLEDBSimpleProvider.setVariant(int iRow, int iColumn, OSPFORMAT format, object Var)
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms714366(v=vs.85)

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.aboutToChangeCell(iRow, iColumn);

            object[] row = _tabularData[iRow];
            row[iColumn - 1] = Var;

            _tabularData[iRow] = row;

            // Notify our Listeners:
            foreach (OLEDBSimpleProviderListener listener in _listListeners)
                listener.cellChanged(iRow, iColumn);

        }

        void OLEDBSimpleProvider.stopTransfer()
        {   // https://docs.microsoft.com/en-us/previous-versions/windows/desktop/ms722788(v=vs.85)
            // we do not handle async ops
        }
    }
}

VBA Client Code

So now you have compiled and registered above C# code now you can run client VBA code. You will need a reference to an ADO type library.

So to get an ADO recordset created by the C# custom implementation one does the following:

  1. Create an ADO.Connection, supply the OLEDB Simple Provider and the ProgID of the C# custom implementation as Data Source;
  2. Create a ADO.RecordSet, set the ActiveConnection property to that created in step (1);
  3. Call the newly created recordSet's Open method supplying the table name.

In given example, the string passed into the RecordSet.Open method is a table name but one can write the code to parse and evaluate a more complicated expression.

Looking through the client code, you'll be able I have commented out some lines which call Recordset.Seek or Recordset.Sort methods because they are not supported and throw errors.

    'rsColors.Seek "Green"  '* throws error "Current provider does not support the necessary interface for Index functionality."
    'rsColors.Sort = "colorName" '* throws error "Current provider does not support the necessary interfaces for sorting or filtering."

But actually you can seek using the Recordset.Filter method. You can add and delete records happily, don't forget to call Recordset.Update. Code to add and delete records is given.

On the matter of sorting you could supply more text to the Recordset.Open method such as a 'Sort By <fieldName>' clause and change the implementation from being a List to a sortable collection class.

In fact you can supply so much more in the string passed to the Recordset.Open method. You could pass a whole SQL command just like other OLEDB providers but you will have to write your own SQL parsing code.

Also in the code below we take a snapshot of the recordset into a variant array. Snapshots are driven by the Recordset.GetRows method. This is also the same logic that drives the Range.CopyFromRecordset method which allows quick writing of a recordset to a block of cells (some commented out code for this is also given).

Enjoy!

Option Explicit

'* Tools->References
'ADODB      Microsoft ActiveX Data Objects 6.1 Library  C:\Program Files (x86)\Common Files\System\ado\msado15.dll

Sub TestCustomSimpleProvider()
    Dim vVarArray
    

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    '*
    '* SimpleOLEDBProvider1.MyDataSource is progid of the custom simple provider
    '*
    oConn.Open "Provider=MSDAOSP;Data Source=SimpleOLEDBProvider1.MyDataSource"
    
    Dim rsColors As ADODB.Recordset
    Set rsColors = New ADODB.Recordset

    Set rsColors.ActiveConnection = oConn

    rsColors.Open "colors"
    
    '*
    '* what it doesn't do:
    '* (a) seeking on an index
    '* (b) sorting
    '*
    
    'rsColors.Seek "Green"  '* throws error "Current provider does not support the necessary interface for Index functionality."
    'rsColors.Sort = "colorName" '* throws error "Current provider does not support the necessary interfaces for sorting or filtering."
    
    '*
    '* what it does do:
    '* (1) filtering
    '* (2) adding
    '* (3) removing
    '*
    
    Debug.Assert rsColors.RecordCount = 3
    rsColors.Filter = "colorName='Green' or colorName='Red'"
    rsColors.Update
    Debug.Assert rsColors.RecordCount = 2
    
    rsColors.Filter = ""
    
    rsColors.AddNew
    rsColors!ColorName = "Yellow"
    rsColors!ColorRGB = "FFFF00"
    rsColors.Update
    
    Debug.Assert rsColors.RecordCount = 4
    
    rsColors.MoveFirst
    rsColors.Delete
    rsColors.Update
    
    Debug.Assert rsColors.RecordCount = 3
    
    
    rsColors.MoveFirst
    vVarArray = Application.WorksheetFunction.Transpose(rsColors.GetRows)
    Stop

    Dim rsCustomers As ADODB.Recordset
    Set rsCustomers = New ADODB.Recordset

    Set rsCustomers.ActiveConnection = oConn
    rsCustomers.Open "customers"

    vVarArray = Application.WorksheetFunction.Transpose(rsCustomers.GetRows)
    Stop
    
    Dim rsOrders As ADODB.Recordset
    Set rsOrders = New ADODB.Recordset
    
    Set rsOrders.ActiveConnection = oConn
    
    rsOrders.Open "Orders"
    vVarArray = Application.WorksheetFunction.Transpose(rsOrders.GetRows)

    '* uncomment the following to test writing whole recordset to worksheet in one go
    'Dim rng As Excel.Range
    'Set rng = ThisWorkbook.Worksheets.Item(1).Cells(1, 1)
    'rsOrders.MoveFirst
    'rng.CopyFromRecordset rsOrders
    
    Stop
End Sub

Links

Sunday, 28 October 2018

OLEDB Simple Provider (OSP) Toolkit Documentation

Introduction

A StackOverflow question prompted me to look for an OLE DB Provider for Xml. This gave the top result of the Microsoft OLE DB Simple Provider | Microsoft Docs. There is some eye-catching text in its description that says

Simple providers are intended to access data sources that require only fundamental OLE DB support, such as in-memory arrays or XML documents.

I blogged the Xml aspect previously, so putting the Xml to one side, the text (I have bolded) says in-memory arrays. I have been looking for some kind of ADO interface for an in memory array for a little while. What is disappointing is that this technology is old so we cannot invest too much time in it. Nevertheless, I have done some surfing and am depositing some findings here on this post.

No Xml please, tell me about In-memory arrays

So I used Google to exclude Xml from results, Provider=MSDAOSP; in memory array -xml - Google Search.

From the link Implementing an ADO Server we can see that the interface guid (IID) is E0E270C0-C0BE-11D0-8FE4-00A0C90A6341}. Looking up this in the registry I find

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Interface\{E0E270C0-C0BE-11D0-8FE4-00A0C90A6341}]
@="OLEDBSimpleProvider"

[HKEY_CLASSES_ROOT\Interface\{E0E270C0-C0BE-11D0-8FE4-00A0C90A6341}\ProxyStubClsid]
@="{00020424-0000-0000-C000-000000000046}"

[HKEY_CLASSES_ROOT\Interface\{E0E270C0-C0BE-11D0-8FE4-00A0C90A6341}\ProxyStubClsid32]
@="{00020424-0000-0000-C000-000000000046}"

[HKEY_CLASSES_ROOT\Interface\{E0E270C0-C0BE-11D0-8FE4-00A0C90A6341}\TypeLib]
@="{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}"
"Version"="1.5"

So in the above there is a type library guid (LIBID) so we can look for that and we find

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}]

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}\1.5]
@="Microsoft OLE DB Simple Provider 1.5 Library"

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}\1.5\0]

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}\1.5\0\win32]
@="C:\\Windows\\SysWOW64\\simpdata.tlb"

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}\1.5\0\win64]
@="C:\\Windows\\System32\\simpdata.tlb"

[HKEY_CLASSES_ROOT\TypeLib\{E0E270C2-C0BE-11D0-8FE4-00A0C90A6341}\1.5\FLAGS]
@="0"

In the above we can see a path to the type library C:\\Windows\\SysWOW64\\simpdata.tlb . An excel workbook's VBA project can make a reference to this type library by selecting Microsoft OLE DB Simple Provider 1.5 Library because happily the type library is restricted to automation types; with a VBA reference one can inspect the type library via the Object Browser. However for fans of IDL and because it can be pasted as one long text document here is the IDL as given by OLEView.exe.

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: simpdata.tlb

[
  uuid(E0E270C2-C0BE-11D0-8FE4-00A0C90A6341),
  version(1.5),
  helpstring("Microsoft OLE DB Simple Provider 1.5 Library")
]
library MSDAOSP
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface OLEDBSimpleProviderListener;
    interface OLEDBSimpleProvider;

    typedef enum {
        OSPFORMAT_RAW = 0,
        OSPFORMAT_DEFAULT = 0,
        OSPFORMAT_FORMATTED = 1,
        OSPFORMAT_HTML = 2
    } OSPFORMAT;

    typedef enum {
        OSPRW_DEFAULT = 1,
        OSPRW_READONLY = 0,
        OSPRW_READWRITE = 1,
        OSPRW_MIXED = 2
    } OSPRW;

    typedef enum {
        OSPFIND_DEFAULT = 0,
        OSPFIND_UP = 1,
        OSPFIND_CASESENSITIVE = 2,
        OSPFIND_UPCASESENSITIVE = 3
    } OSPFIND;

    typedef enum {
        OSPCOMP_EQ = 1,
        OSPCOMP_DEFAULT = 1,
        OSPCOMP_LT = 2,
        OSPCOMP_LE = 3,
        OSPCOMP_GE = 4,
        OSPCOMP_GT = 5,
        OSPCOMP_NE = 6
    } OSPCOMP;

    typedef enum {
        OSPXFER_COMPLETE = 0,
        OSPXFER_ABORT = 1,
        OSPXFER_ERROR = 2
    } OSPXFER;

    [
      odl,
      uuid(E0E270C1-C0BE-11D0-8FE4-00A0C90A6341),
      version(1.4),
      oleautomation
    ]
    interface OLEDBSimpleProviderListener : IUnknown {
        HRESULT _stdcall aboutToChangeCell(
                        [in] long iRow, 
                        [in] long iColumn);
        HRESULT _stdcall cellChanged(
                        [in] long iRow, 
                        [in] long iColumn);
        HRESULT _stdcall aboutToDeleteRows(
                        [in] long iRow, 
                        [in] long cRows);
        HRESULT _stdcall deletedRows(
                        [in] long iRow, 
                        [in] long cRows);
        HRESULT _stdcall aboutToInsertRows(
                        [in] long iRow, 
                        [in] long cRows);
        HRESULT _stdcall insertedRows(
                        [in] long iRow, 
                        [in] long cRows);
        HRESULT _stdcall rowsAvailable(
                        [in] long iRow, 
                        [in] long cRows);
        HRESULT _stdcall transferComplete([in] OSPXFER xfer);
    };

    [
      odl,
      uuid(E0E270C0-C0BE-11D0-8FE4-00A0C90A6341),
      version(1.4),
      oleautomation
    ]
    interface OLEDBSimpleProvider : IUnknown {
        HRESULT _stdcall getRowCount([out, retval] long* pcRows);
        HRESULT _stdcall getColumnCount([out, retval] long* pcColumns);
        HRESULT _stdcall getRWStatus(
                        [in] long iRow, 
                        [in] long iColumn, 
                        [out, retval] OSPRW* prwStatus);
        HRESULT _stdcall getVariant(
                        [in] long iRow, 
                        [in] long iColumn, 
                        [in] OSPFORMAT format, 
                        [out, retval] VARIANT* pVar);
        HRESULT _stdcall setVariant(
                        [in] long iRow, 
                        [in] long iColumn, 
                        [in] OSPFORMAT format, 
                        [in] VARIANT Var);
        HRESULT _stdcall getLocale([out, retval] BSTR* pbstrLocale);
        HRESULT _stdcall deleteRows(
                        [in] long iRow, 
                        [in] long cRows, 
                        [out, retval] long* pcRowsDeleted);
        HRESULT _stdcall insertRows(
                        [in] long iRow, 
                        [in] long cRows, 
                        [out, retval] long* pcRowsInserted);
        HRESULT _stdcall find(
                        [in] long iRowStart, 
                        [in] long iColumn, 
                        [in] VARIANT val, 
                        [in] OSPFIND findFlags, 
                        [in] OSPCOMP compType, 
                        [out, retval] long* piRowFound);
        HRESULT _stdcall addOLEDBSimpleProviderListener([in] OLEDBSimpleProviderListener* pospIListener);
        HRESULT _stdcall removeOLEDBSimpleProviderListener([in] OLEDBSimpleProviderListener* pospIListener);
        HRESULT _stdcall isAsync([out, retval] long* pbAsynch);
        HRESULT _stdcall getEstimatedRows([out, retval] long* piRows);
        HRESULT _stdcall stopTransfer();
    };
};

So in the interface definition we can see some usefully unique keywords which we can use a search terms to search for sample code. And we find a good but old volume Serious ADO: Universal Data Access with Visual Basic - Rob MacDonald - Google Books which gives us some hints about how to write a VB6 component but it becomes apparent that one needs an actual VB6 copy because there is a widget to implement an interface that is otherwise restricted to VB6 developers. I have tracked the type library

MSDATASRC     Microsoft Data Source Interfaces for ActiveX Data Binding Type Library     C:\Windows\SysWOW64\msdatsrc.tlb

And I give the type library's IDL

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: msdatsrc.tlb

[
  uuid(7C0FFAB0-CD84-11D0-949A-00A0C91110ED),
  version(1.0),
  helpstring("Microsoft Data Source Interfaces for ActiveX Data Binding Type Library")
]
library MSDATASRC
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface DataSourceListener;
    interface DataSource;

    typedef [uuid(7C0FFAB1-CD84-11D0-949A-00A0C91110ED), public]
    BSTR DataMember;

    [
      odl,
      uuid(7C0FFAB2-CD84-11D0-949A-00A0C91110ED),
      hidden,
      oleautomation
    ]
    interface DataSourceListener : IUnknown {
        [hidden]
        HRESULT _stdcall dataMemberChanged([in] DataMember bstrDM);
        [hidden]
        HRESULT _stdcall dataMemberAdded([in] DataMember bstrDM);
        [hidden]
        HRESULT _stdcall dataMemberRemoved([in] DataMember bstrDM);
    };

    [
      odl,
      uuid(7C0FFAB3-CD84-11D0-949A-00A0C91110ED),
      oleautomation
    ]
    interface DataSource : IUnknown {
        [restricted, hidden]
        HRESULT _stdcall getDataMember(
                        [in] DataMember bstrDM, 
                        [in] GUID* riid, 
                        [out, retval] IUnknown** ppunk);
        [hidden]
        HRESULT _stdcall getDataMemberName(
                        [in] long lIndex, 
                        [out, retval] DataMember* pbstrDM);
        [hidden]
        HRESULT _stdcall getDataMemberCount([out, retval] long* plCount);
        [hidden]
        HRESULT _stdcall addDataSourceListener([in] DataSourceListener* pDSL);
        [hidden]
        HRESULT _stdcall removeDataSourceListener([in] DataSourceListener* pDSL);
    };
};

So the restricted method on the interface DataSource prevents VBA from implementing this interface. Any source code for VB6 would rely upon a widget. This means that only the Delphi article, Implementing an ADO Server and any C++ source code we can find can give us a clue as to how this can be implemented. It ought to be possible to implement in C#.

API Links

MSDATASRC.DataSource Interface Methods

DataSource Interface and Methods is defined in MSDATASRC namespace as imported by reference to C:\\Windows\\SysWOW64\\msdatsrc.tlb

MSDATASRC.DataSourceListener Interface Methods

DataSourceListener Interface and Methods is defined in MSDATASRC namespace as imported by reference to C:\\Windows\\SysWOW64\\msdatsrc.tlb

MSDAOSP.OLEDBSimpleProvider Interface Methods

OLEDBSimpleProvider Interface and Methods are defined in MSDAOSP namespace as imported by reference to C:\\Windows\\SysWOW64\\simpdata.tlb

MSDAOSP.OLEDBSimpleProviderListener Interface Methods

OLEDBSimpleProviderListener Interface and Methods are defined in MSDAOSP namespace as imported by reference to C:\\Windows\\SysWOW64\\simpdata.tlb

Other OLEDB Links

Other Links

Tuesday, 23 October 2018

OLEDB Simple Provider - C++ Sample Step Thru

It is with delight that I can report that I found a OLEDB Simple Provider C++ sample that Microsoft had deposited in Github for posterity. I got it working with a little tweak and was able to write some client VBA ADO code to retrieve an ADO recordset. I also managed to step through the code to document the interaction.

Context - Data Access Solutions

I have been surveying data access solution for working with Excel worksheets. I have found the Microsoft.ACE.OLEDB provider but I have found some weaknesses with it and was wondering what else is possible. As a caveat, it is worth noting Microsoft say they will remove this feature (OLEDB Simple Provider) from a future version of Windows. Nevertheless, for due diligence this solution ought to be explored (later I intend to investigate .NET managed data providers which will be more future proof).

Context - OLEDB Simple Provider VB6 sample

Also note that I have been investigating trying to write a custom implementation for the OLEDB Simple Provider using VBA. VBA is extremely close to VB6. Unfortunately, VB6 has a few extra mechanisms to assist the interaction with the underlying OLEDB Simple Provider interfaces. These extra mechanisms are simply not present in VBA rendering any VB6 code informative but unusable. So I needed to step through the C++ sample because I want to write a C# implementation.

Because a VBA solution is not possible we need to drop down to lower level, C++, so we can figure out how to write a C# implementation.

Client VBA Code

Below is ordinary client VBA ADO code. The key details are in the ADO connection string and the string passed to ADO recordset's Open method.

The connection is critical to correctly using the OLEDB Simple Provider. First, we first specify Provider=MSDAOSP; in all cases. The second term Data Source is where we specify a COM ProgId of a COM class which implements IDataSource. In this case we pass Data Source=ospsampc because ospsampc is the ProgId of the Windows sample. The registry settings can be found in Appendix A .

The other key detail is what is passed to ADO recordset's Open method. In the sample a file path is passed. However, there is no reason why a different implementation may pass a web url or a COM moniker. In the sample code, the file is opened and processed; it is semicolon separated with a row and column count on the top row (see Appendix B).

Option Explicit

'* Tools->References
'ADODB      Microsoft ActiveX Data Objects 6.1 Library  C:\Program Files (x86)\Common Files\System\ado\msado15.dll

Sub TestCustom()
    
    Const sFileName As String = "C:\Users\Simon\source\repos\Windows-class-samples\Samples\Win7Samples\dataaccess\osp\customer.txt"
    Debug.Assert Dir(sFileName) <> ""

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    '*
    '* ospsampc is progid of the custom simple provider
    '*
    oConn.Open "Provider=MSDAOSP;Data Source=ospsampc"
    
    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    
    Set rs.ActiveConnection = oConn
    
    rs.Open sFileName
    rs.MoveFirst
        
    '* lets get the recordset to a variant array
    Dim vVarArray
    vVarArray = rs.GetRows
End Sub

Tweak to given sample code

So the sample code is at Windows-classic-samples/Samples/Win7Samples/dataaccess/osp/vc at master · Microsoft/Windows-classic-samples · GitHub . It compiles fine for me no problem. One slight tweak is that the output filename needs to be changed. So here are instructions...

  1. Right-click on project icon and from menu select Properties
  2. In Property Pages, select General in the left hand pane
  3. On the right hand pane, change Target Name from $(ProjectName) to ospsampc

Call Graph Walkthrough

The choreography between the classes can be a little confusing so I carefully stepped through the code and annotated it as this is the best way to learn the interaction. What you see below is VBA lines of code interleaved with the C++ sample code that gets called (typically via the OLEDB Simple Provider Dll, msdaosp.dll). Most of it is COM QueryInterface and COM initialisation. I have omitted calls to AddRef, Release and and calls to listening mechanisms as they cluttered the graph.

I recommend reading through the call graph but if you want a summary here it is VBA passes in the conn

  1. VBA passes ADO a connection string to the Connection.Open method, Provider=MSDAOSP;Data Source=ospsampc
  2. ADO sees the Provider=MSDAOSP and knows to load the OLEDB Simple Provider and passes control to its Dll, msdaosp.dll
  3. msdaosp.dll parses the second term of the connection string to find the custom implementation's progid, Data Source=ospsampc
  4. The progid ospsampc is found in the registry and the sample dll, ospsampc.dll, is loaded.
  5. ospsampc.dll is called via entry point DllGetClassObject to get an object that implements IClassFactory.
  6. IClassFactory.CreateInstance is called and ospsampc.dll returns an instance of MyDataSource (whilst creating an instance of MyOSPObject for later).
  7. The instance of MyDataSource is queried for interface IDataSource
  8. Control is passed back to VBA
  9. VBA code creates recordset and sets active connection. No sample code is called for this (must be purely ADO and/or msdaosp.dll).

  10. VBA code passes a filepath as the string parameter to the ADO recordset's Open method.
  11. IDataSource::getDataMember is called with the filepath passed in to bstrDM parameter, this method is implemented by MyDataSource::getDataMember.
  12. MyDataSource::getDataMember calls MyOSPObject::Init on its private instance of MyOSPObject passing the filepath as parameter.
  13. MyOSPObject::Init loads the file into memory
  14. MyDataSource::getDataMember queries MyOSPObject for the OLEDBSimpleProvider interface
  15. The OLEDB Simple provider dll, msdaosp.dll can now call MyOSPObject directly on its OLEDBSimpleProvider interface implementation
  16. msdaosp.dll calls MyOSPObject's OLEDBSimpleProvider interface implementation for row and column count etc as part of initialisation
  17. Control is passed back to VBA
  18. From now on, when VBA calls ADO to manipulate recordset msdaosp.dll makes calls on MyOSPObject's OLEDBSimpleProvider interface implementation.

VBA:    oConn.Open "Provider=MSDAOSP;Data Source=ospsampc"
C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll

 //
 // get the class factory by calling DllGetClassObject
 //
  ospsampc.dll!DllGetClassObject(const _GUID & rclsid, const _GUID & riid, void * * ppvObj) 
   ospsampc.dll!MyClassFactory::MyClassFactory() 
   ospsampc.dll!MyClassFactory::QueryInterface(const _GUID & riid, void * * ppv) 

 //
 // call CreateInstance on class factory, creates a MyDataSource (and during initialisation internally creates a MyOSPObject)
 // returns an IUnknown , no QI for IDataSource yet
 //
  ospsampc.dll!MyClassFactory::CreateInstance(IUnknown * pUnkOuter, const _GUID & riid, void * * ppv) 
   ospsampc.dll!MyDataSource::MyDataSource() 
  ospsampc.dll!MyDataSource::Init() 
    ospsampc.dll!MyOSPObject::MyOSPObject() 
    ospsampc.dll!CExList::CExList() 
  ospsampc.dll!MyDataSource::QueryInterface(const _GUID & riid, void * * ppv) 


 //
 // After some interim calls to QI for IUnknown we soon get a QI for IDataSource
 //
 ospsampc.dll!MyDataSource::QueryInterface(const _GUID & riid, void * * ppv) 

VBA:    Set rs = New ADODB.Recordset
VBA:    Set rs.ActiveConnection = oConn
VBA:    rs.Open sFileName
C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll

 //
 // the parameter passed by VBA in the rs.Open method is passed as bstrDM below
 // in this case a file path which gthis implementation will load and process
 // but the string could be an a block of COM moniker pointing to a block of cells on an Excel worksheet
 //
 // after initialization MyOSPObject is QI'ed for OLEDBSimpleProvider interface
 //
 ospsampc.dll!MyDataSource::getDataMember(wchar_t * bstrDM, const _GUID & riid, IUnknown * * ppUnk) 
  ospsampc.dll!MyOSPObject::Init(wchar_t * pwszFilePath) 
  ospsampc.dll!MyOSPObject::QueryInterface(const _GUID & riid, void * * ppv) 


C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll
 //
 // now msdaosp.dll calls MyOSPObject directly to QI for OLEDBSimpleProvider (again)
 // and then starts calling on OLEDBSimpleProvider::getRowCount , OLEDBSimpleProvider::getColumnCount   etc.
 //
 ospsampc.dll!MyOSPObject::QueryInterface(const _GUID & riid, void * * ppv) Line 87 
 ospsampc.dll!MyOSPObject::getRowCount(long * pcRows) 
 ospsampc.dll!MyOSPObject::getColumnCount(long * pcColumns) 
  ospsampc.dll!MyOSPObject::isAsync(int * pbAsynch)


Links

Appendix A - Registry Entries for sample

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}]
@="ospsampc"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\InprocServer32]
"ThreadingModel"="Both"
@="C:\\Users\\Simon\\source\\repos\\Windows-class-samples\\Samples\\Win7Samples\\dataaccess\\osp\\vc\\Debug\\ospsampc.dll"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\ProgID]
@="ospsampc"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\VersionIndependentProgID]
@="ospsampc"

Appendix B - Sample database file

So the given database file has semicolons separating fields with newline as row terminator. There is also a row and column count on the top row. Also we have a row of column headers. No schema information though. Here at the top rows...

90;10
CustomerID;CompanyName;ContactName;ContactTitle;Address;City;Region;PostalCode;Country;Phone;
ALFKI;Alfreds Futterkiste;Maria Anders;Sales Representative;Obere Str. 57;Berlin;;12209;Germany;030-0074321;
ANATR;Ana Trujillo Emparedados y helados;Ana Trujillo;Owner;Avda. de la Constitución 2222;México D.F.;;05021;Mexico;(5) 555-4729;
ANTON;Antonio Moreno Taquería;Antonio Moreno;Owner;Mataderos  2312;México D.F.;;05023;Mexico;(5) 555-3932;
AROUT;Around the Horn;Thomas Hardy;Sales Representative;120 Hanover Sq.;London;;WA1 1DP;UK;(71) 555-7788;
BERGS;Berglunds snabbköp;Christina Berglund;Order Administrator;Berguvsvägen  8;Luleå;;S-958 22;Sweden;0921-12 34 65;
BLAUS;Blauer See Delikatessen;Hanna Moos;Sales Representative;Forsterstr. 57;Mannheim;;68306;Germany;0621-08460;

Monday, 22 October 2018

OLEDB Simple Provider - Xml Reader

So I have chanced upon something call the OLEDB Simple Provider which allows VBA to convert an Xml document into a recordset. Worth investigating some more in its own right because it is said to support shaped, i.e. hierarchical recordsets. But right now, I want to find how to implement a custom OLEDB Simple Provider for an in memory array, a goal I've had for some time.

It has to be said that a recordset can be saved to Xml and indeed a saved Xml recordset can be synthesised as I did in this blog post.

Option Explicit

'* Tools->References
'MSXML2     Microsoft XML, v6.0                         C:\Windows\SysWOW64\msxml6.dll
'ADODB      Microsoft ActiveX Data Objects 6.1 Library  C:\Program Files (x86)\Common Files\System\ado\msado15.dll

Sub Test()
    Dim xmlDom As MSXML2.DOMDocument60
    Set xmlDom = New MSXML2.DOMDocument60
    
    Dim sColorsXml As String
    sColorsXml = "<colors>" & _
            "<color><colorname>Red</colorname><colorRGB>FF0000</colorRGB></color>" & _
            "<color><colorname>Green</colorname><colorRGB>00FF00</colorRGB></color>" & _
            "<color><colorname>Blue</colorname><colorRGB>0000FF</colorRGB></color></colors>"

    
    xmlDom.LoadXML sColorsXml
    Debug.Assert xmlDom.parseError = 0
    
    Const sFileName As String = "N:Colors.xml"
    xmlDom.Save sFileName

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    'oConn.Open "Provider=MSDAOSP;Data Source=MSXML2.DSOControl.5.0"
    oConn.Open "Provider=MSDAOSP;Data Source=MSXML2.DSOControl"

    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    Set rs.ActiveConnection = oConn
    
    rs.Open sFileName
    rs.MoveFirst
    Debug.Assert rs.RecordCount = 3
    Debug.Assert rs.Fields.Count = 3  '* this surprised me
    Debug.Assert rs.Fields.Item(0).Name = "colorname"
    Debug.Assert rs.Fields.Item(1).Name = "colorRGB"
    Debug.Assert rs.Fields.Item(2).Name = "$Text"  '* this is the additional unexpected field, it concatentates the others
        
    '* lets get the recordset to a variant array
    Dim vVarArray
    vVarArray = Application.WorksheetFunction.Transpose(rs.GetRows)
    Debug.Assert vVarArray(1, 1) = "Red"
    Debug.Assert vVarArray(1, 2) = "FF0000"
    Debug.Assert vVarArray(1, 3) = "RedFF0000"
    Debug.Assert vVarArray(2, 1) = "Green"
    Debug.Assert vVarArray(2, 2) = "00FF00"
    Debug.Assert vVarArray(2, 3) = "Green00FF00"
    Debug.Assert vVarArray(3, 1) = "Blue"
    Debug.Assert vVarArray(3, 2) = "0000FF"
    Debug.Assert vVarArray(3, 3) = "Blue0000FF"
    
    Stop
End Sub

Appendix A - Registry Entries

Just doing some digging into the load sequence etc....

So "MSXML2.DSOControl" is a ProgId and we can go find it in the registry ...

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Msxml2.DSOControl.5.0]
@="XML Data Source Object 5.0"

[HKEY_CLASSES_ROOT\Msxml2.DSOControl.5.0\CLSID]
@="{88D969E9-F192-11D4-A65F-0040963251E5}"

So we can lookup the CLSID, {88D969E9-F192-11D4-A65F-0040963251E5} ...

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{88D969E9-F192-11D4-A65F-0040963251E5}]
@="XML Data Source Object 5.0"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{88D969E9-F192-11D4-A65F-0040963251E5}\InProcServer32]
@="C:\Program Files (x86)\Common Files\Microsoft Shared\OFFICE11\MSXML5.DLL"
"ThreadingModel"="Apartment"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{88D969E9-F192-11D4-A65F-0040963251E5}\ProgID]
@="Msxml2.DSOControl.5.0"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{88D969E9-F192-11D4-A65F-0040963251E5}\TypeLib]
@="{F5078F18-C551-11D3-89B9-0000F81FE221}"

[HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{88D969E9-F192-11D4-A65F-0040963251E5}\Version]
@="5.0"

We can find the type library with {F5078F18-C551-11D3-89B9-0000F81FE221} There were multiple versions, I'm showing version 5 ...

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}]

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}\5.0]
@="Microsoft XML, v5.0"

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}\5.0\0]
@=""

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}\5.0\0\win32]
@="C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\OFFICE11\\MSXML5.DLL"

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}\5.0\FLAGS]
@="0"

[HKEY_CLASSES_ROOT\Wow6432Node\TypeLib\{F5078F18-C551-11D3-89B9-0000F81FE221}\5.0\HELPDIR]
@="C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\OFFICE11\\"

Loading the type library C:\Program Files (x86)\Common Files\microsoft shared\OFFICE11.MSXML5.DLL to see what interfaces DSOControl supports we can see it only has one


[
  uuid(88D969E9-F192-11D4-A65F-0040963251E5),
  helpstring("XML Data Source Object")
]
coclass DSOControl50 {
    [default] interface IDSOControl;
};


[
  odl,
  uuid(310AFA62-0575-11D2-9CA9-0060B0EC3D39),
  helpstring("DSO Control"),
  hidden,
  dual,
  nonextensible,
  oleautomation
]
interface IDSOControl : IDispatch {
    [id(0x00010001), propget]
    HRESULT XMLDocument([out, retval] IXMLDOMDocument** ppDoc);
    [id(0x00010001), propput]
    HRESULT XMLDocument([in] IXMLDOMDocument* ppDoc);
    [id(0x00010002), propget]
    HRESULT JavaDSOCompatible([out, retval] long* fJavaDSOCompatible);
    [id(0x00010002), propput]
    HRESULT JavaDSOCompatible([in] long fJavaDSOCompatible);
    [id(0xfffffdf3), propget]
    HRESULT readyState([out, retval] long* state);
};

And there the trail goes cold, nothing interesting about that interface, certainly nothing that can help our custom implementation of OLEDB Simple Provider.

Tuesday, 9 October 2018

VBA - Microsoft.ACE.OLEDB.12.0 details

I investigated Microsoft.ACE.OLEDB.12.0 and have plenty of artefacts and findings. In case you haven't met this component it allows data to be read and written to Excel worksheets using SQL technology.

Nomenclature

As far as I can see ACE stands for Access Connectivity Engine. This wikipedia article is a good web page which highlights the history of the name.

COM Registry entries

Some time back (with some help from StackOverflow) I got the ATL C++ Sample OLEDB Provider compiled and working. From that experience I can tell you that every provider string is in fact a COM Prog ID. This means we call write code like this to test the installation...

Private Sub TestInstallation()
    Dim iunkOleDb As IUnknown
    Set iunkOleDb = VBA.CreateObject("Microsoft.ACE.OLEDB.12.0")  '<--- this would error if not installed
End Sub

It also means if we scan the registry for the Prog ID "Microsoft.ACE.OLEDB.12.0" then we can find other details. I have placed a registry export of the COM registry entries in Appendix A.

From the details it can be seen that the ProgId is 'Microsoft.ACE.OLEDB.12.0' whilst the fuller name is 'Microsoft Office 12.0 Access Database Engine OLE DB Provider' and it is implemented in the executable ACEOLEDB.DLL. This gives us some search terms to google on.

Installation

If you need to install this then you must download the Microsoft Access Database Engine 2010 Redistributable . The accompanying explanatory text says this is not a replacement for Jet saying one should use SQL Server Express Edition but to be honest I think many of us do see Microsoft.ACE.OLEDB.12.0 as a Jet replacement.

Also on that download page there some help about how to use an Extended Property in the connection string to specify the correct file format version.

File Type (extension)                               Extended Properties
---------------------------------------------------------------------------------------------
Excel 97-2003 Workbook (.xls)                       "Excel 8.0"
Excel 2007-2010 Workbook (.xlsx)                    "Excel 12.0 Xml"
Excel 2007-2010 Macro-enabled workbook (.xlsm)      "Excel 12.0 Macro"
Excel 2007-2010 Non-XML binary workbook (.xlsb)     "Excel 12.0"

As it turns out, one can supply a wider range of values than those shown above, i.e. non-Excel file formats. Appendix D shows a screenshot of the registry which I believe shows all the valid values, they are all ISAM Formats.

Pitfall - Workbooks needs to be saved

I suspect the code in the provider is contingent on the workbook's file extension and it will complain if it has no file extension. When you create a workbook, it is just "Book1" ; it has no file extension until it has been saved at least once. This pitfall is easily countered with a line of defensive code to inspect the workbook's file extension ...

    Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0  '* Workbook needs a file extension, i.e. saved at least once!

... or ...

    If UBound(Split(ThisWorkbook.Name, ".")) = 0 Then Err.Raise vbObjectError, , "#Workbook needs a file extension, i.e. saved at least once!"

Connection Strings Resources

An excellent resource for how to build a connection string for any data provider is www.connectionstrings.com and on that link one can see connection strings for historic versions of Excel. Also on that page are details of extended properties.

Jet Extended Properties

I'd like to compile a list of extended properties that relate to Microsoft.ACE.OLEDB.12.0 . I suspect many of them are inherited from the Jet. So here is a list of Jet extended properties courtesy of Working with MS Excel(xls / xlsx) Using MDAC and Oledb - CodeProject, a great article that I won't try and replicate.

Looks like Extended Properties needs enclosing double quotes (in some cases at least).

  • HDR - Short for Header, if YES then the top row are like column headers and interpreted as field names.
  • ReadOnly
  • FirstRowHasNames - different way to do same as HDR
  • MaxScanRows - data types are inferred from n rows, this sets n
  • IMEX - I'm guessing this is short for Import/Export and is also used in column type inference

Related to IMEX is ImportMixedTypes which I have seen in an Microsoft.ACE.OLEDB.12.0 connection string but not in a Jet connection string. For Jet and Microsoft.ACE.OLEDB.12.0 ImportMixedTypes is a registry entry but it also looks like supplying ImportMixedTypes in the Microsoft.ACE.OLEDB.12.0 connection string allows an override. For explanation of ImportMixedTypes here is another great article, this time at dailydoseofexcel.com, Daily Dose of Excel - External Data – Mixed Data Types .

Pitfall - The Problem of Type Inference

So the OLEDB Provider infers a column's data type from its contents, sampling the data. I don't much like this, I'd prefer a way to specify the data type but I have yet to find a way to do this. Perhaps it is best to ensure the data in the cells is consistent, we can lock sheets and control access to ensure a user does not corrupt the data but then that creates a need to show a separate data entry form. I will mull this. In the meantime I'd advise you are very disciplined that any data you write is type consistent for that column.

Access Connectivity Engine

So I have discovered another bunch of registry entries which I placed in Appendix B. So there is another dll at work here, ACEEXCL.DLL. I will try to investigate how ACEEXCL.DLL interacts with ACEOLEDB.DLL. UPDATE: I solved this in Appendix D!

Pitfall - Pass CursorTypeEnum.adOpenKeyset or CursorTypeEnum.adOpenStatic When Opening a Recordset

Even after correctly forming a connection strings I have still had some issues using this OLEDB provider. So in in my use case when calling the Recordset.Open method it is critical to pass the right enumeration value. CursorTypeEnum.adOpenDynamic and CursorTypeEnum.adOpenForwardOnly did not throw errors they simply returned an empty recordset! This matters because I believe one of them is the assumed default. I needed to pass either CursorTypeEnum.adOpenKeyset or CursorTypeEnum.adOpenStatic to get any rows back.

Pitfall - Better To Specify an Exact Range Than a Whole Sheet

Even after sorting a connection string and CursorTypeEnum parameter one can still get bugs. If a whole sheet is specified then it will infer data from the whole Worksheet.UsedRange. This means if you dirty your cells on the sheet (by entering anything and deleting them) then that cell and all those between it and $A$1 will be implied to belong to the table. So it is better to find the range with [A1].CurrentRegion.Address and either (1) define a name over that range and pass range name into the SQL or (2) used the explicit address of the range, e.g. $A$1:$B$3

Sample Code to Open a Recordset

So now we know where the pitfalls lie we can write some defensive sample code. This code opens a recordset and prints out its contents. PLEASE USE A FRESH NEW WORKBOOK! There is some setup code to write some data to a sheet in SetUpSomeData() so best to use a new workbook but remember to save the workbook at least once.

The code demonstrates the following points ...

  • It defends against the pitfalls of unsaved workbooks;
  • it supplies a working CursorTypeEnum;
  • it restricts the cells to select, by two different methods (1) by name and (2) by cell address

As a bonus I have added some code in ReadExcelCatalog which demonstrates using the ADOX library to read schema information so one can tell exactly what the OLEDB provider is inferring for a column type. Enjoy!.

Option Explicit
Option Private Module

'* Tools -> References
'* ADODB  Microsoft ActiveX Data Objects 6.1 Library  C:\Program Files (x86)\Common Files\System\ado\msado15.dll
'* ADOX   Microsoft ADO Ext. 6.0 for DDL and Security C:\Program Files (x86)\Common Files\System\ado\msadox.dll

Private Sub SetUpSomeData()
    '* WARNING this will wipe data!
    Dim sht As Excel.Worksheet
    Set sht = ThisWorkbook.Worksheets.Item("Sheet1")
    sht.Cells.Clear
    
    '*
    '* use our array literal trick, for more tricks tips and 'blue sky thinking'
    '* see http://exceldevelopmentplatform.blogspot.com
    '*
    Dim vData As Variant
    vData = [{"Color","RGB";"Red","FF0000";"Green","00FF00"}]

    sht.Range("A1:B3").Value2 = vData

End Sub

Private Sub TestInstallation()

    Dim iunkOleDb As IUnknown
    Set iunkOleDb = VBA.CreateObject("Microsoft.ACE.OLEDB.12.0")  '<--- this would error if not installed

End Sub


Private Sub ReadData()
    '* Code here assumes there is data in top left of Sheet1, call SetUpSomeData() first if you have no data
    'Call SetUpSomeData

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    If UBound(Split(ThisWorkbook.Name, ".")) = 0 Then Err.Raise vbObjectError, , "#Workbook needs a file extension, i.e. saved at least once!"
    
    'Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0  '* Workbook needs a file extension, i.e. saved at least once!
    
    oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
           "Data Source=" & ThisWorkbook.FullName & ";" & _
           "Extended Properties='Excel 12.0 Macro;HDR=YES'"
           
    
    Dim sht As Excel.Worksheet
    Set sht = ThisWorkbook.Worksheets.Item("Sheet1")
    
    
    '*
    '* Limit the range to the block of cells contigous with A1
    '*
    Dim rngTable As Excel.Range
    Set rngTable = sht.Cells(1, 1).CurrentRegion
    
    
    '*
    '* Case 1 - using Named Range
    '* (use separate recordset)
    '*
    Dim rsByName As ADODB.Recordset
    Set rsByName = New ADODB.Recordset
    
    ThisWorkbook.Names.Add "MyTable", rngTable
    
    Dim sCmdTextUsingName As String
    sCmdTextUsingName = "Select * From MyTable"
    
    rsByName.Open sCmdTextUsingName, oConn, CursorTypeEnum.adOpenStatic '* can use CursorTypeEnum.adOpenKeyset
    Debug.Assert rsByName.RecordCount > 0
    
    
    
    '*
    '* Case 2 - using Cell Addresses
    '* (use separate recordset)
    '*
    Dim rsByCellAddress As ADODB.Recordset
    Set rsByCellAddress = New ADODB.Recordset
    
    Dim sCmdTextUsingCellAddress As String
    sCmdTextUsingCellAddress = "Select * From [" & sht.Name & "$" & rngTable.Address(False, False, xlA1) & "]"
    
    rsByCellAddress.Open sCmdTextUsingCellAddress, oConn, CursorTypeEnum.adOpenStatic '* can use CursorTypeEnum.adOpenKeyset
    Debug.Assert rsByCellAddress.RecordCount > 0
    

    '*
    '* output one of the recordsets (they should be identical)
    '*
    DumpRecordset rsByCellAddress

End Sub

Private Sub DumpRecordset(ByVal rs As ADODB.Recordset)
    '*
    '* Some code to iterate over the recordset
    '*
    rs.MoveFirst
    
    Dim lFieldCount As Long
    lFieldCount = rs.Fields.Count
    
    While Not rs.EOF
            
        Dim sOutputLine As String
        sOutputLine = ""
            
        Dim sFieldAndValue As String
        Dim lFieldLoop As Long
        For lFieldLoop = 0 To lFieldCount - 1
            sFieldAndValue = rs.Fields.Item(lFieldLoop).Name & ":" & rs.Fields.Item(lFieldLoop).Value
            
            sOutputLine = sOutputLine & VBA.IIf(Len(sOutputLine) > 0, vbTab, "") & sFieldAndValue
        Next
        Debug.Print sOutputLine
        rs.MoveNext
    Wend

End Sub


Private Sub ReadExcelCatalog()
    '*
    '* Some code to give the schema details such as columns names and columns types (what is inferred rathe than what is defined)
    '*
    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    If UBound(Split(ThisWorkbook.Name, ".")) = 0 Then Err.Raise vbObjectError, , "#Workbook needs a file extension, i.e. saved at least once!"
    
    'Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0  '* Workbook needs a file extension, i.e. saved at least once!
    
    oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
           "Data Source=" & ThisWorkbook.FullName & ";" & _
           "Extended Properties='Excel 12.0 Macro;HDR=YES'"
    
    Dim catDB As ADOX.Catalog
    Set catDB = New ADOX.Catalog
    Set catDB.ActiveConnection = oConn
    
    Dim adoxTableLoop As ADOX.Table
    For Each adoxTableLoop In catDB.Tables
        If adoxTableLoop.Name = "MyTable" Then
            Dim adoxColumnLoop As ADOX.Column
            For Each adoxColumnLoop In adoxTableLoop.Columns
                Debug.Print adoxColumnLoop.Name & vbTab & Switch(adoxColumnLoop.Type = adVarWChar, "String", adoxColumnLoop.Type = adDouble, "Double")
            Next
        End If
    Next

End Sub

Links

Appendix A - COM Registry entries

It always useful to poke around in the registry to see what makes something tick, here is a registry export of the related keys. It turns out there is a second bunch of registry keys to tune the behaviour (page down). The following set of registry keys fulfil the COM registration requirements for OLEDB providers.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}]
"OLEDB_SERVICES"=dword:fffffffe
@="Microsoft.ACE.OLEDB.12.0"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}\ExtendedErrors]
@="Microsoft.ACE.OLEDBErrors.12.0"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}\ExtendedErrors\{3BE786A0-0366-4F5C-9434-25CF162E475F}]
@="Microsoft.ACE.OLEDBErrors.12.0"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}\InprocServer32]
@="C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\OFFICE15\\ACEOLEDB.DLL"
"ThreadingModel"="Both"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}\OLE DB Provider]
@="Microsoft Office 12.0 Access Database Engine OLE DB Provider"

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Classes\Wow6432Node\
CLSID\{3BE786A0-0366-4F5C-9434-25CF162E475E}\ProgID]
@="Microsoft.ACE.OLEDB.12.0"

Appendix B - Access Connectivity Engine Registry entries

So I have discovered another bunch of registry entries which I found after discovering this page Initializing the Microsoft Excel Driver -MSDN.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Office\15.0\ClickToRun\REGISTRY\MACHINE\Software\Wow6432Node\
Microsoft\Office\15.0\Access Connectivity Engine\Engines\Excel]
"DisabledExtensions"="!xls"
"ImportMixedTypes"="Text"
"FirstRowHasNames"=hex:01
"AppendBlankRows"=dword:00000001
"TypeGuessRows"=dword:00000008
"win32"="C:\\Program Files (x86)\\Common Files\\Microsoft Shared\\OFFICE15\\ACEEXCL.DLL"

Appendix C - Access Connectivity Engine Files and Dependencies

So there is a whole bunch of files prefixed with ACE*.DLL which look related to Access Connectivity Engine, for me they are located in

C:\Program Files\Microsoft Office 15\root\vfs\ProgramFilesCommonX86\Microsoft Shared\OFFICE15

which looks like some sort of virtualised file system (is that what the vfs stands for?). Anyway, here is the list

 Directory of C:\Program Files\Microsoft Office 15\root\vfs\ProgramFilesCommonX86\Microsoft Shared\OFFICE15

1,680,128 ACECORE.DLL   'depends on OS files OLE32.DLL, ADVAPI32.DLL, KERNEL32.DLL, OLEAUT32.DLL and C++ Files MSVCR100.DLL and MSVCP100.DLL
  432,384 ACEDAO.DLL    'depends on OS files OLE32.DLL, ADVAPI32.DLL, KERNEL32.DLL, OLEAUT32.DLL and C++ Files MSVCR100.DLL 
   35,032 ACEERR.DLL    'depends on OS files OLE32.DLL, ADVAPI32.DLL, KERNEL32.DLL               and C++ Files MSVCR100.DLL 
  633,688 ACEES.DLL     'depends on OS files OLE32.. ADVAPI32.. KERNEL32.. OLEAUT32.. VERSION.DLL and MSVCR100.. MSVCP100..
  186,600 ACEEXCH.DLL   'depends on ACECORE.DLL ; OS files OLE32.. ADVAPI32.. KERNEL32.. OLEAUT32..  and MSVCR100.. 
  400,184 ACEEXCL.DLL   'depends on ACECORE.DLL ; OS files OLE32.. ADVAPI32.. KERNEL32.. OLEAUT32..  and MSVCR100..  MSVCP100..
  278,256 ACEODBC.DLL   'depends OS files GDI32.DLL OLE32.. ADVAPI32.. KERNEL32.. COMDLG32.DLL  and MSVCR100..  
   15,000 ACEODEXL.DLL  'depends on ACEODBC.DLL ; OS file KERNEL32..  and MSVCR100..  
   15,016 ACEODTXT.DLL  'depends on ACEODBC.DLL ; OS file KERNEL32..  and MSVCR100..  
  329,552 ACEOLEDB.DLL  'depends on OS files OLE32.DLL, ADVAPI32.DLL, KERNEL32.DLL, OLEAUT32.DLL and C++ Files MSVCR100.DLL 
  161,400 ACETXT.DLL    'depends on ACECORE.DLL ; OS files OLE32.. ADVAPI32.. KERNEL32.. OLEAUT32..  and C++ MSVCR100..  MSVCP100..
3,049,184 ACEWDAT.DLL   'depends on OS file KERNEL32.DLL and C++ File MSVCR100.DLL 

I do not know what ACEES.DLL or ACEWDAT.DLL are but all the other files we can guess at their purpose.

ACECORE.DLL   'The core library for Access Connectivity Engine (ACE)
ACEDAO.DLL    'The DAO (Data Access Objects) companion file for ACE
ACEERR.DLL    '?A repository of error messages?
ACEEXCH.DLL   'The Microsoft Exchange companion/driver file for ACE
ACEEXCL.DLL   'The Microsoft Excel companion/driver file for ACE via OLEDB
ACEODBC.DLL   'The ODBC (Open Database Connectivity) companion file for ACE
ACEODEXL.DLL  'The Microsoft Excel companion/driver file for ACE via ODBC
ACEODTXT.DLL  'The Textfile companion/driver file for ACE via ODBC
ACEOLEDB.DLL  'The core OLEDB ACE file
ACETXT.DLL    'The Textfile companion/driver file for ACE via OLEDB

We know the route into the code starts with COM and ACEOLEDB.DLL (see Appendix A), looking at the entry points for ACEOLEDB.DLL we see the classic COM entry points

DllCanUnloadNow
DllGetClassObject
DllMain

If we look at the entry point for ACEEXCL.DLL we see the classic COM entry points

DllGetClassObject

so very much a COM DLL. I wonder what classes are created and passed out by these DLLs. OLEVIEW sheds no light on this or ACEOLEDB.DLL

Appendix D - ISAM Formats (first term of Extended Properties) Map to Engines

Below is a screenshot of the registry which I believe shows all the valid values for the first term of the Extended Properties. They are all ISAM Formats.

Looking down the list of Value Data pairs for the given key, 'Excel 12.0 Macro', we can see one entry 'Engine' with 'Excel' as the string data. The 'HTML Export' and 'HTML Import' keys also have 'Engine' Values with 'Text' as the string data. In another screenshot we can see that they must be mapping to the keys under the Engines key. I have drawn some mapping lines (sorry no arrow heads).

Let's look at what is the Excel engine key. Voila, it tells which DLL to load to handle requests for Excel in the win32 value ...ACEEXCL.DLL . The Value-Data pairs shown below in the next screenshot have already been detailed in Appendix B but it is only now that I have pieced together the logic sequence to the load the right 'engine' file.

Friday, 7 September 2018

VBA - XML - OLEDB - US Treasuries Web Service

So the code below will call the web service of the United States Treasury department that publishes Yield Curve data (bond interest rates). I wanted the data so I wrote this program and I might as well share. The code below shows Xml being parsed using the standard Xml libraries available to the VBA developer. The Xml is parsed into a two dimensional array which is pasted onto a sheet called Batch.

Excel's OLEDB Provider supports outer joins and INSERT INTO SELECT

So most of the code is concerned with the Xml parsing and writing the data to Batch sheet. However, I need some logic to update the Master sheet. The Master sheet is meant to contain all previous results not just the batch of data acquired. The Master sheet must not have duplicates. Appending records to the Master is a task ideally suited for Microsoft Access and other database technologies. In the past I might have written VBA to loop through the rows individually to establish if Master doesn't yet have that record before appending it by pasting to the bottom.

But this is a good case to use Excel's OLEDB Provider. In ANSI SQL there is the INSERT INTO SELECT sql statement which selects from one table and inserts into another. But I want no duplicates so I use an outer join and test for nulls. I am pleased to say Excel's OLEDB Provider can handle this (whereas the deprecated JET driver might not have) and that this is achieved in so few lines of code, here it is ...

Sub UpdaterMaster()

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0  '* Workbook needs to be saved
    
    oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
           "Data Source=" & ThisWorkbook.FullName & ";" & _
           "Extended Properties='Excel 12.0 Macro'"

    oConn.Execute "INSERT INTO [Master$] Select B.* from [Batch$] AS B LEFT join [Master$] as M on B.Date=M.Date where IsNull(M.Date )"
    
    SortMaster
End Sub

Actually, it is one magic line, highlighted in blue.

In the future, for tabular data processing on worksheets I will always look first at Excel's OLEDB Provider to see if it is capable. So much code saved!

Caveat

The Master sheet cannot be empty. So to get going you need to copy over manually the first batch. That could be automated.

Full Listing

The full listing is shown here

Option Explicit
    
Private mvData()
Private mlRowCount As Long

Public Enum ycfYieldCurveFeed
    ycfId = 1
    ycfNEW_DATE
    ycfBC_1MONTH
    ycfBC_3MONTH
    ycfBC_6MONTH
    ycfBC_1YEAR
    ycfBC_2YEAR
    ycfBC_3YEAR
    ycfBC_5YEAR
    ycfBC_7YEAR
    ycfBC_10YEAR
    ycfBC_20YEAR
    ycfBC_30YEAR
    ycfBC_30YEARDISPLAY
    ycfMin = ycfId
    ycfMax = ycfBC_30YEARDISPLAY
End Enum

Function BatchSheet() As Excel.Worksheet
    Set BatchSheet = ThisWorkbook.Worksheets("Batch")
End Function

Function MasterSheet() As Excel.Worksheet
    Set MasterSheet = ThisWorkbook.Worksheets("Master")
End Function

Sub GetTreasuryData()

    Dim shBatch As Excel.Worksheet
    Set shBatch = BatchSheet

    Dim oXHR As MSXML2.XMLHTTP60
    Set oXHR = New MSXML2.XMLHTTP60
    
    mlRowCount = 0
    ReDim mvData(ycfMin To ycfMax, 0 To mlRowCount)
    AddColumnHeadings
    
    Dim lYearLoop As Long
    For lYearLoop = 2018 To 2018
    
        Dim dtStart As Date
        dtStart = DateSerial(lYearLoop, 1, 1)
        
        Dim dtEnd As Date
        dtEnd = DateSerial(lYearLoop, 12, 31)
        
        shBatch.Cells.ClearContents
        
        Dim dtLoop As Date
        For dtLoop = dtStart To dtEnd
            
            DoEvents
            Debug.Print VBA.FormatDateTime(dtLoop, vbLongDate)
            Dim lWeekday As Long
            lWeekday = Weekday(dtLoop, vbSunday)
            
            If Not (lWeekday = 1 Or lWeekday = 7) Then
        
                Dim sURL As String
                sURL = USTreasuryUrl(dtLoop)
            
                oXHR.Open "GET", sURL, False
                oXHR.send
                
                Dim xmlPage As MSXML2.DOMDocument60
                Set xmlPage = New MSXML2.DOMDocument60
                xmlPage.LoadXML oXHR.responseText
                
                xmlPage.setProperty "SelectionNamespaces", "xmlns:ust='http://www.w3.org/2005/Atom' xmlns:m='http://schemas.microsoft.com/ado/2007/08/dataservices/metadata' xmlns:d='http://schemas.microsoft.com/ado/2007/08/dataservices'"
                
                Dim xmlEntries As MSXML2.IXMLDOMNodeList
                Set xmlEntries = xmlPage.SelectNodes("ust:feed/ust:entry/ust:content/m:properties")
                
                'Dim dtLastSnapDate As Date
                
                Dim xmlEntryLoop As MSXML2.IXMLDOMElement
                For Each xmlEntryLoop In xmlEntries
                
                    mlRowCount = mlRowCount + 1
                    ReDim Preserve mvData(ycfMin To ycfMax, 0 To mlRowCount)
                    
                    Dim xmlProps As MSXML2.IXMLDOMNodeList
                    Set xmlProps = xmlEntryLoop.SelectNodes("*")
                    
                    Dim xmlProp As MSXML2.IXMLDOMElement
                    For Each xmlProp In xmlProps
                        
                        Dim sType As String
                        sType = xmlProp.getAttribute("m:type")
                        
                        If xmlProp.getAttribute("m:null") = True Then
                            '*skip the null
                        Else
                            If StrComp(sType, "Edm.Int32", vbTextCompare) = 0 Then
                                mvData(ycfId, mlRowCount) = CLng(xmlProp.nodeTypedValue)
                                
                            ElseIf StrComp(sType, "Edm.DateTime", vbTextCompare) = 0 Then
                                Dim vSplitDate As Variant
                                vSplitDate = VBA.Split(xmlProp.nodeTypedValue, "T")
                                Dim vSplit2 As Variant
                                vSplit2 = Split(vSplitDate(0), "-")
                                Dim dtSnapDate As Date
                                dtSnapDate = DateSerial(vSplit2(0), vSplit2(1), vSplit2(2))
                                
                                'Debug.Assert dtSnapDate > dtLastSnapDate
                                'dtLastSnapDate = dtSnapDate
                                mvData(ycfNEW_DATE, mlRowCount) = CLng(dtSnapDate)

                            ElseIf StrComp(sType, "Edm.Double", vbTextCompare) = 0 Then
                                mvData(LookupColumnOrdinal(xmlProp.BaseName), mlRowCount) = CDbl(xmlProp.nodeTypedValue)
                            Else
                                Stop '*unrecognized
                            End If
                        
                        End If
                    
                    Next
                
                Next xmlEntryLoop
            
            End If
        Next
        
    Next lYearLoop
    Dim rng As Excel.Range
    Set rng = shBatch.Range(shBatch.Cells(1, 1), shBatch.Cells(mlRowCount + 1, ycfMax))
    rng.Value = Application.WorksheetFunction.Transpose(mvData)

    UpdaterMaster
End Sub



Sub UpdaterMaster()

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    Debug.Assert UBound(Split(ThisWorkbook.Name, ".")) > 0  '* Workbook needs to be saved
    
    oConn.Open "Provider=Microsoft.ACE.OLEDB.12.0;" & _
           "Data Source=" & ThisWorkbook.FullName & ";" & _
           "Extended Properties='Excel 12.0 Macro'"

    oConn.Execute "INSERT INTO [Master$] Select B.* from [Batch$] AS B LEFT join [Master$] as M on B.Date=M.Date where IsNull(M.Date )"
    
    SortMaster
End Sub

Sub SortMaster()

    Dim wsMasters As Excel.Worksheet
    Set wsMasters = MasterSheet

    Dim rngTable As Excel.Range
    Set rngTable = wsMasters.Cells(1, 1).CurrentRegion
    
    Dim rngKey As Excel.Range
    Set rngKey = rngTable.Columns(2).Resize(rngTable.Rows.Count - 1).Offset(1)
    
    
    wsMasters.Sort.SortFields.Clear
    wsMasters.Sort.SortFields.Add Key:=rngKey _
        , SortOn:=xlSortOnValues, Order:=xlAscending, DataOption:=xlSortNormal
    With wsMasters.Sort
        .SetRange rngTable
        .Header = xlYes
        .MatchCase = False
        .Orientation = xlTopToBottom
        .SortMethod = xlPinYin
        .Apply
    End With

End Sub

Private Function USTreasuryUrl(ByVal dtSnapDate As Date) As String

    Dim lDay As Long
    lDay = Day(dtSnapDate)
    
    Dim lMonth As Long
    lMonth = Month(dtSnapDate)
     
    Dim lYear As Long
    lYear = Year(dtSnapDate)

    Dim sTemplate As String
    sTemplate = "http://data.treasury.gov/feed.svc/DailyTreasuryYieldCurveRateData?$filter=day(NEW_DATE) eq $DAY$ and month(NEW_DATE) eq $MONTH$ and year(NEW_DATE) eq $YEAR$"
    
    USTreasuryUrl = Replace(Replace(Replace(Replace(sTemplate, "$DAY$", CStr(lDay)), "$MONTH$", CStr(lMonth)), "$YEAR$", CStr(lYear)), " ", " ")
    

End Function

Private Function LookupColumnOrdinal(ByVal sBaseName As String) As ycfYieldCurveFeed
    Static dicLookup As Scripting.Dictionary
    If dicLookup Is Nothing Then
        Set dicLookup = New Scripting.Dictionary
        dicLookup.CompareMode = TextCompare
        
        dicLookup.Add "Id", ycfId
        dicLookup.Add "NEW_DATE", ycfNEW_DATE
        dicLookup.Add "BC_1MONTH", ycfBC_1MONTH
        dicLookup.Add "BC_3MONTH", ycfBC_3MONTH
        dicLookup.Add "BC_6MONTH", ycfBC_6MONTH
        dicLookup.Add "BC_1YEAR", ycfBC_1YEAR
        dicLookup.Add "BC_2YEAR", ycfBC_2YEAR
        dicLookup.Add "BC_3YEAR", ycfBC_3YEAR
        dicLookup.Add "BC_5YEAR", ycfBC_5YEAR
        dicLookup.Add "BC_7YEAR", ycfBC_7YEAR
        dicLookup.Add "BC_10YEAR", ycfBC_10YEAR
        dicLookup.Add "BC_20YEAR", ycfBC_20YEAR
        dicLookup.Add "BC_30YEAR", ycfBC_30YEAR
        dicLookup.Add "BC_30YEARDISPLAY", ycfBC_30YEARDISPLAY
    End If

    Debug.Assert dicLookup.Exists(sBaseName)
    LookupColumnOrdinal = dicLookup.Item(sBaseName)
End Function

Private Sub AddColumnHeadings()
    mvData(ycfId, 0) = "Id"
    mvData(ycfNEW_DATE, 0) = "Date"
    mvData(ycfBC_1MONTH, 0) = "1M"
    mvData(ycfBC_3MONTH, 0) = "3M"
    mvData(ycfBC_6MONTH, 0) = "6M"
    mvData(ycfBC_1YEAR, 0) = "1Y"
    mvData(ycfBC_2YEAR, 0) = "2Y"
    mvData(ycfBC_3YEAR, 0) = "3Y"
    mvData(ycfBC_5YEAR, 0) = "5Y"
    mvData(ycfBC_7YEAR, 0) = "7Y"
    mvData(ycfBC_10YEAR, 0) = "10Y"
    mvData(ycfBC_20YEAR, 0) = "20Y"
    mvData(ycfBC_30YEAR, 0) = "30Y"
    mvData(ycfBC_30YEARDISPLAY, 0) = "*"
End Sub