Over the last few days, I've been exploring the abilities and limits of ADO disconnected recordsets. These are memory-based recordsets that have no need for any physical database, and can be particularly useful in certain circumstances. Their largest advantage (over, say, a Collection or array of UDTs) is that many of the members (methods and properties) of an ADO recordset become available to us. Just a few of these would include the Sort, Filter, and Clone methods.
All is attached to a demo project, but I'll also list the code in this post.
To make these disconnected recordsets easier to understand and create, I've written an enumeration that isolates the field types particularly useful to VB6. In addition, I've written a procedure, for creating these recordsets.
See the attached project, and the Form1 code for several examples that use the above.
I've also created an RTF document (within the attached project) that outlines the uses and limits of each of the field types in the AdoMemDataTypeEnum enumeration. Here are the contents of that document:
---------------
A few nice references:
This document focuses on using ADO disconnected recordsets for which we have no intention of mirroring anything found in an actual database table. In other words, discussed are those cases where we wish more functionality than we might achieve with an array of UDTs or possibly a collection of objects. Disconnected ADO recordsets have several features which just aren't available in an array (or Collection or Dictionary objects).
This is a document that accompanies the ADO_Disconnected.bas module. There is a procedure named CreateMemRecordset within that module, which makes creating ADO disconnected recordsets quite easy. Once they're created, many of the typical ADO recordset members (methods & properties) can be used to manipulate them.
To destroy these disconnected ADO recordsets, you can either close them (with the recordset Close method) or you can uninstantiate the the recordset object (by setting the last recordset object variable to Nothing, or assigning another recordset to it). Either the Close or un-instantiation is sufficient for cleanup, making the recordset no longer usable, and cleans up everything.
A detailed discussion of the use of all the ADO recordset members is beyond the scope of this document, but several can be seen in the Form1.frm test module. However, I will go over a few things:
The Update method doesn't seem to do anything at all, but can be used as a type of comment if you like (for completing the addition of a record, or altering it).
The Delete and CancelUpdate methods for these disconnected recordsets seem to do the exact same thing. And CancelUpdate works (just as Delete) even after moving the recordset's record cursor.
We can always alter (re-assign) the values in the record of a recordset so long as the recordset's cursor is pointing at a valid record (i.e., not EOF or BOF). In a certain sense, these disconnected ADO recordsets are similar to an array of UDTs.
A primary purpose for the following is to document the ADO field types (for a disconnected recordset) that actually make sense within VB6 usage. To assist with this, a cut-down version of the complete ADO DataTypeEnum, called AdoMemDataTypeEnum (as seen in the module) was created. Here is this cut-down enum:
Basically, this document will discuss each of the field types:
The following types work pretty much exactly as they do in VB6, and are all stored directly in the ADO records: adoByte, adoBoolean, adoInteger, adoLong, adoCurrency, adoSingle, adoDouble, & adoDate. They have the same levels of precision, and will overflow with errors just as VB6 will when using the corresponding types.
There is also an adoDecimal type. There is no Decimal type in VB6, but you can get one of these Decimal types into a VB6 Variant. Or, you can alternatively create a Decimal type on-the-fly with the CDec() function, possibly converting from a String. Beyond that, so long as you assign them into ADO records from a Variant containing a Decimal, or you retrieve them from an ADO record by placing them back into a Variant, they work pretty much the same as the more fundamental types (such as adoLong or adoDouble). The adoDecimal type does not sort correctly. It doesn't throw an error, but the recordset isn't sorted according to the values in the adoDecimal field.
The adoString type is truly equivalent to a VB6 String type. It's Unicode, it can be variable length, and it goes into an ADO record's field from a VB6 String (or back out into a VB6 String) absolutely fine. However, there are a couple of difference between this adoString and something like adoLong. First, we can't sort a recordset on one of these adoString fields. If sorting is needed on a text field, the adoChar or adoWChar fields are recommended (discussed below). Second, the ADO record holds only a pointer to the actual BSTR string. And third, clean-up of these fields is a bit more involved than the more fundamental types (adoLong, etc.), and we'll discuss this now:
Clean-up of ADO fields containing pointers:
Next on the list are the adoChar and adoWChar field types. In both cases, the data for these fields is contained within the record (as opposed to the way adoString fields are handled), and we can also sort on these fields. We typically assign values to these fields using a VB6 string. However, the downside of these type field types is that they will error if you overflow the length. As such, these are also the only two field types that require a specification of FieldSize during their creation. This is the number of characters (not bytes) allowed in the field (and you don't need to worry about any null termination characters).
The distinction between adoChar and adoWChar is that adoChar is ANSI and adoWChar is Unicode. Any VB6 string (so long as it's not too long) will go into adoWChar. However, only VB6 strings that can be converted to ANSI (without any characters lost in the Unicode-to-ANSI conversion) will go into adoChar. If there's a Unicode character that can't be converted to ANSI in the VB6 string, and you attempt to assign it to an adoChar field, it will error.
The adoObject and adoIUnknown are next on the enum list. These are also pointers in the ADO record, so the above clean-up of ADO fields containing pointers rules apply. These are also fundamental types within VB6 (Object and IUnknown). The primary distinction between the two is that an Object carries a reference to an object that could be early-bound, whereas that may not be the case for IUnknown. However, a full distinction is beyond our scope herein.
The important thing for us here is that these adoObject and adoIUnknown types can carry references to objects within an ADO record. If the object is instantiated, the object's reference count is incremented when an object's reference is placed into an ADO record's field. If that field is subsequently set to Nothing, the reference count is decremented. Or, if the recordset is closed (or all references to the recordset are set to Nothing), all object references within the recordset will have their counts decremented.
The adoVariant type can also hold objects, and, when it does, it follows the same rules as adoObject and adoIUnknown. It can also hold all the more basic types (Long, Double, etc), and works just fine when it does. It can also hold a string, and follows the rules of adoString when it does. But it can do one more thing ... it can hold arrays, just as a VB6 Variant can hold an array. These arrays are pointers in the adoVariant, so, once again, they follow the rules of Clean-up of ADO fields containing pointers. This is one case where it may be particularly important to clean up, or you risk leaving orphaned arrays in memory (in the case of Delete without first setting field to Empty), but they will eventually clean up when the recordset object is either closed or uninstantiated.
Sorting a disconnected ADO recordset:
The Save method for these disconnected ADO recordsets:
---------------
Enjoy,
Elroy
All is attached to a demo project, but I'll also list the code in this post.
To make these disconnected recordsets easier to understand and create, I've written an enumeration that isolates the field types particularly useful to VB6. In addition, I've written a procedure, for creating these recordsets.
Code:
'
' The ADO version 2.8 was used for all testing (msado28.tlb),
'
' This is just a bit of code to reduce the ADO to be more "obvious" for creating
' memory-based (aka, disconnected) recordsets.
' See ADO_Disconnected.rtf for uses, abilities, & limits of these disconnected ADO recordsets.
' which should be available on all later versions of Windows.
'
Option Explicit
'
Public Enum AdoMemDataTypeEnum ' A slight renaming of the ADO field types, to make the list better suited for VB6 types.
'
' You are STRONGLY encouraged to read through the ADO_Disconnected.rtf document
' to develop a full understanding of how to use each of these field types with
' a disconnected ADO recordset.
'
adoByte = adUnsignedTinyInt ' One byte.
adoBoolean = adBoolean ' Two bytes (special case of a vbInteger).
adoInteger = adSmallInt ' Two bytes.
adoLong = adInteger ' Four bytes.
adoCurrency = adCurrency ' Eight bytes.
adoSingle = adSingle ' Four bytes.
adoDouble = adDouble ' Eight bytes.
adoDate = adDate ' Eight bytes (special case of a vbDouble).
adoDecimal = adDecimal ' Fourteen bytes.
adoString = adBSTR ' Pointer is 4 bytes, data is variable.
adoChar = adChar ' ANSI. FieldSize (characters) required.
adoWChar = adWChar ' Unicode. FieldSize (characters) required.
adoObject = adIDispatch ' For holding object references in a recordset. SEE ADO_Disconnected.rtf for uses/limits.
adoIUnknown = adIUnknown ' For holding object references in a recordset. SEE ADO_Disconnected.rtf for uses/limits.
adoVariant = adVariant ' Sixteen bytes in the recordset. SEE ADO_Disconnected.rtf for uses/limits.
End Enum
#If False Then ' Intellisense fix.
Dim adoByte, adoBoolean, adoInteger, adoLong, adoCurrency, adoSingle, adoDouble
Dim adoDecimal, adoDate, adoString, adoWChar, adoChar, adoObject, adoIUnknown, adoVariant
#End If
'
Public Type AdoMemFieldDefsType
FieldName As String
FieldType As AdoMemDataTypeEnum
FieldSize As Long ' Only needed for character fields (adWChar & adChar).
End Type
'
Public Function CreateMemRecordset(FieldDefs() As AdoMemFieldDefsType) As ADODB.Recordset
' Fields() must be dimensioned, or error.
' See ADO_Disconnected.rtf for more details.
'
' To destroy the recordset, either close it or de-instantiate the recordset variable(s).
'
Set CreateMemRecordset = New ADODB.Recordset ' Because we're creating it from scratch, no concern over Connection nor locks nor cursor location.
With CreateMemRecordset
.CursorLocation = adUseClient
With .Fields
Dim iPtr As Long
For iPtr = LBound(FieldDefs) To UBound(FieldDefs)
Select Case FieldDefs(iPtr).FieldType
Case adoByte, adoBoolean, adoInteger, adoLong, adoCurrency, adoSingle, adoDouble, adoDate
.Append FieldDefs(iPtr).FieldName, FieldDefs(iPtr).FieldType
Case adoDecimal
.Append FieldDefs(iPtr).FieldName, FieldDefs(iPtr).FieldType
CreateMemRecordset.Fields(FieldDefs(iPtr).FieldName).Precision = 29& ' Mantissa accuracy in base 10.
CreateMemRecordset.Fields(FieldDefs(iPtr).FieldName).NumericScale = 28& ' Fraction decimal places in base 10.
Case adoWChar, adoChar ' Size required.
.Append FieldDefs(iPtr).FieldName, FieldDefs(iPtr).FieldType, FieldDefs(iPtr).FieldSize
Case adoString
.Append FieldDefs(iPtr).FieldName, FieldDefs(iPtr).FieldType
Case adoObject, adoIUnknown, adoVariant
.Append FieldDefs(iPtr).FieldName, FieldDefs(iPtr).FieldType
Case Else
Err.Raise 5& ' Some disallowed type was specified.
End Select
Next
End With
.Open
End With
End Function
I've also created an RTF document (within the attached project) that outlines the uses and limits of each of the field types in the AdoMemDataTypeEnum enumeration. Here are the contents of that document:
---------------
Documentation for the ADO_Disconnected.bas Module
A few nice references:
https://learn.microsoft.com/en-us/sq...l-server-ver16
https://renenyffenegger.ch/notes/dev...ts/field/index
https://learn.microsoft.com/en-us/pr...60348(v=vs.60)
https://renenyffenegger.ch/notes/dev...ts/field/index
https://learn.microsoft.com/en-us/pr...60348(v=vs.60)
This document focuses on using ADO disconnected recordsets for which we have no intention of mirroring anything found in an actual database table. In other words, discussed are those cases where we wish more functionality than we might achieve with an array of UDTs or possibly a collection of objects. Disconnected ADO recordsets have several features which just aren't available in an array (or Collection or Dictionary objects).
This is a document that accompanies the ADO_Disconnected.bas module. There is a procedure named CreateMemRecordset within that module, which makes creating ADO disconnected recordsets quite easy. Once they're created, many of the typical ADO recordset members (methods & properties) can be used to manipulate them.
To destroy these disconnected ADO recordsets, you can either close them (with the recordset Close method) or you can uninstantiate the the recordset object (by setting the last recordset object variable to Nothing, or assigning another recordset to it). Either the Close or un-instantiation is sufficient for cleanup, making the recordset no longer usable, and cleans up everything.
A detailed discussion of the use of all the ADO recordset members is beyond the scope of this document, but several can be seen in the Form1.frm test module. However, I will go over a few things:
The Update method doesn't seem to do anything at all, but can be used as a type of comment if you like (for completing the addition of a record, or altering it).
The Delete and CancelUpdate methods for these disconnected recordsets seem to do the exact same thing. And CancelUpdate works (just as Delete) even after moving the recordset's record cursor.
We can always alter (re-assign) the values in the record of a recordset so long as the recordset's cursor is pointing at a valid record (i.e., not EOF or BOF). In a certain sense, these disconnected ADO recordsets are similar to an array of UDTs.
A primary purpose for the following is to document the ADO field types (for a disconnected recordset) that actually make sense within VB6 usage. To assist with this, a cut-down version of the complete ADO DataTypeEnum, called AdoMemDataTypeEnum (as seen in the module) was created. Here is this cut-down enum:
Code:
Public Enum AdoMemDataTypeEnum
adoByte = adUnsignedTinyInt ' One byte.
adoBoolean = adBoolean ' Two bytes (special case of a vbInteger).
adoInteger = adSmallInt ' Two bytes.
adoLong = adInteger ' Four bytes.
adoCurrency = adCurrency ' Eight bytes.
adoSingle = adSingle ' Four bytes.
adoDouble = adDouble ' Eight bytes.
adoDate = adDate ' Eight bytes (special case of a vbDouble).
adoDecimal = adDecimal ' Not clear on how many record bytes.
adoString = adBSTR ' Pointer is 4 bytes, data is variable.
adoChar = adChar ' ANSI. FieldSize (characters) required.
adoWChar = adWChar ' Unicode. FieldSize (characters) required.
adoObject = adIDispatch ' For holding object references in a recordset.
adoIUnknown = adIUnknown ' For holding object references in a recordset.
adoVariant = adVariant ' Sixteen bytes in the recordset.
End Enum
The following types work pretty much exactly as they do in VB6, and are all stored directly in the ADO records: adoByte, adoBoolean, adoInteger, adoLong, adoCurrency, adoSingle, adoDouble, & adoDate. They have the same levels of precision, and will overflow with errors just as VB6 will when using the corresponding types.
There is also an adoDecimal type. There is no Decimal type in VB6, but you can get one of these Decimal types into a VB6 Variant. Or, you can alternatively create a Decimal type on-the-fly with the CDec() function, possibly converting from a String. Beyond that, so long as you assign them into ADO records from a Variant containing a Decimal, or you retrieve them from an ADO record by placing them back into a Variant, they work pretty much the same as the more fundamental types (such as adoLong or adoDouble). The adoDecimal type does not sort correctly. It doesn't throw an error, but the recordset isn't sorted according to the values in the adoDecimal field.
The adoString type is truly equivalent to a VB6 String type. It's Unicode, it can be variable length, and it goes into an ADO record's field from a VB6 String (or back out into a VB6 String) absolutely fine. However, there are a couple of difference between this adoString and something like adoLong. First, we can't sort a recordset on one of these adoString fields. If sorting is needed on a text field, the adoChar or adoWChar fields are recommended (discussed below). Second, the ADO record holds only a pointer to the actual BSTR string. And third, clean-up of these fields is a bit more involved than the more fundamental types (adoLong, etc.), and we'll discuss this now:
Clean-up of ADO fields containing pointers:
This would include adoString fields, adoObject, adoIUnknown, and any adoVariant field with the variant containing a string, object, or array.
Anytime the recordset is closed, or all recordset reference variables are set to Nothing (or overwritten with another object), all of these pointer fields are cleaned up.
When the Delete method is used on a recordset (to delete a specific record), these pointers are not cleaned up (but they will be when the recordset is closed or completely dereferenced). If you wish to clean these pointers up just prior to deleting a record, any adoString field can be set to vbNullString, any object field (adoObject or adoIUnknown) can be set to Nothing, and any adoVariant field could be set to Empty. Then, a Delete could be executed, and all would be cleaned up.
Anytime the recordset is closed, or all recordset reference variables are set to Nothing (or overwritten with another object), all of these pointer fields are cleaned up.
When the Delete method is used on a recordset (to delete a specific record), these pointers are not cleaned up (but they will be when the recordset is closed or completely dereferenced). If you wish to clean these pointers up just prior to deleting a record, any adoString field can be set to vbNullString, any object field (adoObject or adoIUnknown) can be set to Nothing, and any adoVariant field could be set to Empty. Then, a Delete could be executed, and all would be cleaned up.
Next on the list are the adoChar and adoWChar field types. In both cases, the data for these fields is contained within the record (as opposed to the way adoString fields are handled), and we can also sort on these fields. We typically assign values to these fields using a VB6 string. However, the downside of these type field types is that they will error if you overflow the length. As such, these are also the only two field types that require a specification of FieldSize during their creation. This is the number of characters (not bytes) allowed in the field (and you don't need to worry about any null termination characters).
The distinction between adoChar and adoWChar is that adoChar is ANSI and adoWChar is Unicode. Any VB6 string (so long as it's not too long) will go into adoWChar. However, only VB6 strings that can be converted to ANSI (without any characters lost in the Unicode-to-ANSI conversion) will go into adoChar. If there's a Unicode character that can't be converted to ANSI in the VB6 string, and you attempt to assign it to an adoChar field, it will error.
The adoObject and adoIUnknown are next on the enum list. These are also pointers in the ADO record, so the above clean-up of ADO fields containing pointers rules apply. These are also fundamental types within VB6 (Object and IUnknown). The primary distinction between the two is that an Object carries a reference to an object that could be early-bound, whereas that may not be the case for IUnknown. However, a full distinction is beyond our scope herein.
The important thing for us here is that these adoObject and adoIUnknown types can carry references to objects within an ADO record. If the object is instantiated, the object's reference count is incremented when an object's reference is placed into an ADO record's field. If that field is subsequently set to Nothing, the reference count is decremented. Or, if the recordset is closed (or all references to the recordset are set to Nothing), all object references within the recordset will have their counts decremented.
The adoVariant type can also hold objects, and, when it does, it follows the same rules as adoObject and adoIUnknown. It can also hold all the more basic types (Long, Double, etc), and works just fine when it does. It can also hold a string, and follows the rules of adoString when it does. But it can do one more thing ... it can hold arrays, just as a VB6 Variant can hold an array. These arrays are pointers in the adoVariant, so, once again, they follow the rules of Clean-up of ADO fields containing pointers. This is one case where it may be particularly important to clean up, or you risk leaving orphaned arrays in memory (in the case of Delete without first setting field to Empty), but they will eventually clean up when the recordset object is either closed or uninstantiated.
Sorting a disconnected ADO recordset:
The easiest fields on which to sort are adoChar, adoWChar, adoByte, adoBoolean, adoInteger, adoLong, adoCurrency, adoSingle, adoDouble, & adoDate. Executing the Sort method on other fields may or may not throw an error. However, there are certain cases where an error won't be thrown, but the sort order won't be correct. Therefore, it is advisable to stick to these fields for sorting.
The Save method for these disconnected ADO recordsets:
There is a persistence (i.e., Save method) option with these disconnected recordsets. However, it has limits. Basically, if there are any fields with pointers in them, attempting to use the Save method will result in an error. The one exception to this is adoString fields, as they will work just fine with the Save method. However, any/all other fields with pointers are not allowed to be persistent. This would include a recordset with adoObject or adoIUnknown fields, or an adoVariant field containing an object or array. However, as with the adoString fields, if the adoVariant field contains a string, the Save method works just fine. So basically, no objects nor arrays can be persistent with this ADO detached recordset approach.
---------------
Enjoy,
Elroy