Older versions of the ERP package that I work with stored lengthy text data in LONG RAW columns. Newer versions of the ERP package store lengthy text data in BLOB columns. When tables containing those columns are queried using the Microsoft Query tool, which allows the data stored in tables to be retrieved into an Excel worksheet, the text found within the LONG RAW and BLOB columns appears in the Microsoft Query preview window, but those columns are silently excluded when the data is brought back into Microsoft Excel. There must be a work around. Sure, there is an easy solution for BLOB columns using a combination of the UTL_RAW.CAST_TO_VARCHAR2 and DBMS_LOB.SUBSTR functions, but is there a solution for LONG RAW columns?
First, we will build a table to somewhat mimic a table found in the old version of the ERP package (I think that I only omitted the WORKORDER_SUB_ID column):
CREATE TABLE T5( WORKORDER_TYPE CHAR(1), WORKORDER_BASE_ID VARCHAR2(30), WORKORDER_LOT_ID VARCHAR2(3), WORKORDER_SPLIT_ID VARCHAR2(3), SEQUENCE_NO NUMBER(12,0), TYPE CHAR(1), BITS_LENGTH NUMBER(12,0), BITS LONG RAW);
Now, let’s build a bit of code using the Visual Basic for Applications macro editor in Excel to populate the table. I will use late binding, like what is required in VBS macros, so that I can use ADO without adding ADO references in the Visual Basic editor (see the previous articles in the Excel category for directions that permit early binding).
Const adCmdText = 1 Const adCmdStoredProc = 4 Const adParamInput = 1 Const adVarNumeric = 139 Const adBigInt = 20 Const adDecimal = 14 Const adDouble = 5 Const adInteger = 3 Const adLongVarBinary = 205 Const adNumeric = 131 Const adSingle = 4 Const adSmallInt = 2 Const adTinyInt = 16 Const adUnsignedBigInt = 21 Const adUnsignedInt = 19 Const adUnsignedSmallInt = 18 Const adUnsignedTinyInt = 17 Const adDate = 7 Const adDBDate = 133 Const adDBTimeStamp = 135 Const adDBTime = 134 Const adVarChar = 200 Const adUseClient = 3 Const adOpenKeyset = 1 Const adLockOptimistic = 3 Sub CreateRows() Dim i As Integer Dim strUsername As String Dim strPassword As String Dim strDatabase As String Dim strSQL As String Dim intLength As Integer Dim strTempBits As String Dim bytBits() As Byte 'An array of bytes Dim dynData As Object 'ADO Recordset object used to retrieve the user's data Dim dbDatabase As Object 'ADO database connection object On Error Resume Next Set dynData = CreateObject("ADODB.Recordset") Set dbDatabase = CreateObject("ADODB.Connection") strDatabase = "MyDB" strUsername = "MyUser" strPassword = "MyPassword" 'Connect to the database 'Oracle connection string dbDatabase.ConnectionString = "Provider=OraOLEDB.Oracle;Data Source=" & strDatabase & ";User ID=" & strUsername & ";Password=" & strPassword & ";ChunkSize=1000;FetchSize=100;" dbDatabase.ConnectionTimeout = 40 dbDatabase.CursorLocation = adUseClient dbDatabase.Open 'Retrieve a recordset with 0 rows that will allow us to insert into the table strSQL = "SELECT" strSQL = strSQL & " WORKORDER_TYPE," & vbCrLf strSQL = strSQL & " WORKORDER_BASE_ID," & vbCrLf strSQL = strSQL & " WORKORDER_LOT_ID," & vbCrLf strSQL = strSQL & " WORKORDER_SPLIT_ID," & vbCrLf strSQL = strSQL & " SEQUENCE_NO," & vbCrLf strSQL = strSQL & " TYPE," & vbCrLf strSQL = strSQL & " BITS_LENGTH," & vbCrLf strSQL = strSQL & " BITS" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " T5" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " 1=2" dynData.Open strSQL, dbDatabase, adOpenKeyset, adLockOptimistic, adCmdText For i = 1 To 1000 dynData.AddNew dynData("workorder_type") = "W" dynData("workorder_base_id") = "WO" & String(6 - Len(Format(i)), "0") & Format(i) dynData("workorder_lot_id") = "1" dynData("workorder_split_id") = "0" dynData("sequence_no") = 10 dynData("type") = "D" strTempBits = "WO" & String(6 - Len(Format(i)), "0") & Format(i) & "/" & "1" & " This is a very long description." & String(1000, ".") bytBits = StrConv(strTempBits, vbFromUnicode) intLength = Len(strTempBits) dynData("bits_length") = intLength dynData.Fields("bits").AppendChunk bytBits dynData.Update Next i dynData.Close dbDatabase.Close Set dynData = Nothing Set dbDatabase = Nothing End Sub
When the above code is executed, the test table T5 should contain 1,000 rows. Now let’s see if we are able to retrieve the rows from the database into Excel’s worksheet. We will start out by switching to the Data tab in Excel (2010) and then select From Microsoft Query:
The next step is to select a suitable (32 bit, even on 64 bit computers) ODBC entry for the database.
Now let’s enter a simple SQL statement to retrieve the data contained in the test table T5 – note that Microsoft Query will complain if the ; character is included at the end of the SQL statement, but this is done to try to keep Microsoft Query from attempting to re-write the SQL statement (this occasionally eliminates a couple of problems):
As you can see, the Microsoft Query preview window shows the text data that is contained within the LONG RAW BITS column as we had hoped:
Now if we tell Microsoft Query to return the rows to Excel, we see that the LONG RAW BITS column was thrown away:
No problem, we will just try a trick to convert the LONG RAW column to a BLOB using the TO_LOB function and then convert the resulting BLOB to a VARCHAR2. However, that trick simply does not work because the TO_LOB function can only be used in a INSERT INTO … SELECT, or CREATE TABLE … AS SELECT type SQL statement according to the documentation:
So, let’s create a new table to allow us to temporarily convert the LONG RAW column to a BLOB column:
Now, back in Microsoft Query, we change the SQL statement as follows:
SELECT WORKORDER_TYPE, WORKORDER_BASE_ID, WORKORDER_LOT_ID, WORKORDER_SPLIT_ID, SEQUENCE_NO, TYPE, BITS_LENGTH, UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(BITS,32000,1)) BITS FROM T5_TEMP WHERE TYPE='D';
When we return the query results into Excel this time, the LONG RAW column that was converted to a BLOB column in the second table, has its column values converted to a VARCHAR2, and those values actually makes it into Excel (although the column alias “BITS” is lost):
But, I don’t want to go through the process of creating a table to temporarily hold the results of a LONG RAW to BLOB conversion so that I can display the characters in Excel – that would be far too messy if there were many tables. If we tell Excel to record a macro while we bring in data using the Microsoft Query tool, we see a macro that looks like this:
With ActiveSheet.ListObjects.Add(SourceType:=0, Source:=Array(Array( _ "ODBC;DSN=MyODBC;UID=MyUser;;DBQ=MyDB;DBA=W;APA=T;EXC=F;FEN=T;QTO=F;FRC=10;FDL=10;LOB=T;RST=T;BTD=F;BNF=F;BAM=IfAllSuccessful;NUM=NL" _ ), Array("S;DPM=F;MTS=T;MDI=F;CSR=F;FWC=F;FBS=64000;TLO=O;MLD=0;ODA=F;")), _ Destination:=Range("$A$1")).QueryTable .CommandText = Array( _ "SELECT" & Chr(13) & "" & Chr(10) & " WORKORDER_TYPE," & Chr(13) & "" & Chr(10) & " WORKORDER_BASE_ID," & Chr(13) & "" & Chr(10) & _ " WORKORDER_LOT_ID," & Chr(13) & "" & Chr(10) & " WORKORDER_SPLIT_ID," & Chr(13) & "" & Chr(10) & " SEQUENCE_NO," & _ Chr(13) & "" & Chr(10) & " TYPE," & Chr(13) & "" & Chr(10) & " BITS_LENGTH," & Chr(13) & "" & Chr(10) & _ " UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(BITS,32000,1)) " & Chr(13) & "" & Chr(10) & "FROM" & Chr(13) & "" & Chr(10) & "" _ , " T5_TEMP" & Chr(13) & "" & Chr(10) & "WHERE" & Chr(13) & "" & Chr(10) & " TYPE='D';") .RowNumbers = False .FillAdjacentFormulas = False .PreserveFormatting = True .RefreshOnFileOpen = False .BackgroundQuery = True .RefreshStyle = xlInsertDeleteCells .SavePassword = False .SaveData = True .AdjustColumnWidth = True .RefreshPeriod = 0 .PreserveColumnInfo = True .ListObject.DisplayName = "Table_Query_from_OR1125" .Refresh BackgroundQuery:=False End With
While the above is interesting (and probably explains why the BITS column alias was lost), it is probably of little help for our problem. We need something better, our own macro, something like the following (note that the following macro writes the data to a text file, and then uses an Excel function to quickly bring that text file into an Excel worksheet. This approach should be much faster than visiting each cell in the worksheet to write the query results to each cell).
Sub DisplayData() Dim i As Integer Dim lngRow As Long Dim intFileNum As Integer Dim strUsername As String Dim strPassword As String Dim strDatabase As String Dim strSQL As String Dim strOut As String Dim intLength As Integer Dim strTempBits As String Dim snpData As Object 'ADO Recordset object used to retrieve the user's data Dim dbDatabase As Object 'ADO database connection object On Error Resume Next Set snpData = CreateObject("ADODB.Recordset") Set dbDatabase = CreateObject("ADODB.Connection") strDatabase = "MyDB" strUsername = "MyUser" strPassword = "MyPassword" 'Connect to the database 'Oracle connection string dbDatabase.ConnectionString = "Provider=OraOLEDB.Oracle;Data Source=" & strDatabase & ";User ID=" & strUsername & ";Password=" & strPassword & ";ChunkSize=1000;FetchSize=100;" dbDatabase.ConnectionTimeout = 40 dbDatabase.CursorLocation = adUseClient dbDatabase.Open 'Retrieve a recordset with 0 rows that will allow us to insert into the table strSQL = "SELECT" strSQL = strSQL & " WORKORDER_TYPE," & vbCrLf strSQL = strSQL & " WORKORDER_BASE_ID," & vbCrLf strSQL = strSQL & " WORKORDER_LOT_ID," & vbCrLf strSQL = strSQL & " WORKORDER_SPLIT_ID," & vbCrLf strSQL = strSQL & " SEQUENCE_NO," & vbCrLf strSQL = strSQL & " TYPE," & vbCrLf strSQL = strSQL & " BITS_LENGTH," & vbCrLf strSQL = strSQL & " BITS" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " T5" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " TYPE='D'" snpData.Open strSQL, dbDatabase Application.ScreenUpdating = False lngRow = 1 intFileNum = FreeFile Open "C:\LongRawToVarchar.txt" For Output As #intFileNum strOut = "" For i = 0 To snpData.Fields.Count - 1 strOut = strOut & snpData.Fields(i).Name & vbTab 'ActiveSheet.Cells(lngRow, i + 1).Value = snpData.Fields(i).Name Next i Print #intFileNum, strOut Do While Not snpData.EOF lngRow = lngRow + 1 strOut = "" For i = 0 To snpData.Fields.Count - 2 'All Except the last column If Not (IsNull(snpData.Fields(i).Value)) Then 'Switch out Ascii 13 & Ascii 10 combinations for just Ascii 10 so that line breaks do not cause problems in the resulting file strOut = strOut & Replace(snpData.Fields(i).Value, vbCrLf, vbLf) & vbTab 'ActiveSheet.Cells(lngRow, i + 1).Value = snpData.Fields(i).Value Else strOut = strOut & vbTab End If Next i 'Handle the LONG RAW column strTempBits = Replace(StrConv(snpData("bits"), vbUnicode), vbCrLf, vbLf) strOut = strOut & strTempBits & vbTab 'ActiveSheet.Cells(lngRow, snpData.Fields.Count).Value = strTempBits Print #intFileNum, strOut snpData.MoveNext Loop snpData.Close 'Close the data file Close #intFileNum 'Read the text file just written to disk into the worksheet With ActiveSheet.QueryTables.Add(Connection:="TEXT;C:\LongRawToVarchar.txt", Destination:=ActiveSheet.Range("A1")) .Name = "Page1" .FieldNames = True .RowNumbers = False .FillAdjacentFormulas = False .PreserveFormatting = True .RefreshOnFileOpen = False .RefreshStyle = xlInsertDeleteCells .SavePassword = False .SaveData = True .AdjustColumnWidth = True .RefreshPeriod = 0 .TextFilePromptOnRefresh = False .TextFileStartRow = 1 .TextFileParseType = xlDelimited .TextFileTextQualifier = xlTextQualifierDoubleQuote .TextFileConsecutiveDelimiter = False .TextFileTabDelimiter = True 'Tabs are the delimiter .TextFileSemicolonDelimiter = False .TextFileCommaDelimiter = False .TextFileSpaceDelimiter = False .TextFileColumnDataTypes = Array(1) .Refresh BackgroundQuery:=False End With Application.ScreenUpdating = True dbDatabase.Close Set snpData = Nothing Set dbDatabase = Nothing End Sub
If we did not want to write the data out to a text file, we could just comment out the section titled as “‘Read the text file just written to disk into the worksheet“, comment out the Print# lines, and uncomment the lines that begin with “‘ActiveSheet.Cells(“.
For example, if we do not want to write the results to a temp file, our macro would look like this:
Sub DisplayData2() Dim i As Integer Dim lngRow As Long Dim strUsername As String Dim strPassword As String Dim strDatabase As String Dim strSQL As String Dim strOut As String Dim intLength As Integer Dim strTempBits As String Dim snpData As Object 'ADO Recordset object used to retrieve the user's data Dim dbDatabase As Object 'ADO database connection object On Error Resume Next Set snpData = CreateObject("ADODB.Recordset") Set dbDatabase = CreateObject("ADODB.Connection") strDatabase = "MyDB" strUsername = "MyUser" strPassword = "MyPassword" 'Connect to the database 'Oracle connection string dbDatabase.ConnectionString = "Provider=OraOLEDB.Oracle;Data Source=" & strDatabase & ";User ID=" & strUsername & ";Password=" & strPassword & ";ChunkSize=1000;FetchSize=100;" dbDatabase.ConnectionTimeout = 40 dbDatabase.CursorLocation = adUseClient dbDatabase.Open 'Retrieve a recordset with 0 rows that will allow us to insert into the table strSQL = "SELECT" strSQL = strSQL & " WORKORDER_TYPE," & vbCrLf strSQL = strSQL & " WORKORDER_BASE_ID," & vbCrLf strSQL = strSQL & " WORKORDER_LOT_ID," & vbCrLf strSQL = strSQL & " WORKORDER_SPLIT_ID," & vbCrLf strSQL = strSQL & " SEQUENCE_NO," & vbCrLf strSQL = strSQL & " TYPE," & vbCrLf strSQL = strSQL & " BITS_LENGTH," & vbCrLf strSQL = strSQL & " BITS" & vbCrLf strSQL = strSQL & "FROM" & vbCrLf strSQL = strSQL & " T5" & vbCrLf strSQL = strSQL & "WHERE" & vbCrLf strSQL = strSQL & " TYPE='D'" snpData.Open strSQL, dbDatabase Application.ScreenUpdating = False lngRow = 1 strOut = "" For i = 0 To snpData.Fields.Count - 1 ActiveSheet.Cells(lngRow, i + 1).Value = snpData.Fields(i).Name Next i Do While Not snpData.EOF lngRow = lngRow + 1 strOut = "" For i = 0 To snpData.Fields.Count - 2 'All Except the last column If Not (IsNull(snpData.Fields(i).Value)) Then 'Switch out Ascii 13 & Ascii 10 combinations for just Ascii 10 so that line breaks do not cause problems in the resulting file strOut = strOut & Replace(snpData.Fields(i).Value, vbCrLf, vbLf) & vbTab ActiveSheet.Cells(lngRow, i + 1).Value = snpData.Fields(i).Value Else strOut = strOut & vbTab End If Next i 'Handle the LONG RAW column strTempBits = Replace(StrConv(snpData("bits"), vbUnicode), vbCrLf, vbLf) strOut = strOut & strTempBits & vbTab ActiveSheet.Cells(lngRow, snpData.Fields.Count).Value = strTempBits snpData.MoveNext Loop snpData.Close Application.ScreenUpdating = True dbDatabase.Close Set snpData = Nothing Set dbDatabase = Nothing End Sub
The resulting worksheet would then need a bit more clean up than it did when we wrote out the data to a text file and brought the text file into the worksheet with a QueryTable:
So, now you know how to retrieve text contained in a LONG RAW column (our T5 table) or text contained in a BLOB column (our T5_TEMP table, as stored by the recent releases of the ERP package) and display the text in an Excel worksheet with other columns retrieved by a query.
I fully recognize that LONG RAW columns are deprecated, but is anyone able to identify a more direct way to transform a LONG RAW column value into a VARCHAR2 using just a SQL statement with built-in Oracle Database functions, without requiring an intermediate temporary table?
Leave a Reply