gmapiSamples
Mark Juras
Sample Applications of gmAPI
This section contains samples of gmAPI applications.
Migration of VB6 RDO code to ADO.NET
The topic gmsl.Samples.RDOtoNet presents a detailed discussion of a migration of the external library "MSRDO20.DLL#Microsoft Remote Data Object 2.0" to the .NET class "System.Data.SqlClient". That implementation depended on gmSL COM Event Handlers along with extensive modifications of the standard interface description file MSRDO20.dll.xml. This topic performs the same migration using a C# class msrdo that simply uses the standard IDF. This discussion assumes familiarity with the gmSL discussion of this migration.
Within the gmAPI the migration of external libraries makes heavy use of the CodeGroup class. This class was originally developed for use with gmNI and has no direct equivalent in gmSL.
A Simple C# Program was Written
The first step was to write a simple gmAPI based program called "Trans_RDOtoNet" that produces both the unmigrated and migrated translation of the sample code.
namespace GMapi { public class Translate { [STAThread] static void Main(string[] args) { gmBasic.StartUp(commandArgs : args, iStart : 0); RDOtoNet_csh(); RDOtoNet_mig(); gmBasic.Terminate(); } private static void RDOtoNet_csh() { Execute.Storage(action : "Create", identifier : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_std"); Select.DevEnv = "VS2013"; Select.Dialect = Dialects.csh; Select.BuildFile = BuildFileStatus.on; Select.System = @"\gmManual\Samples\FromIdl"; Select.Target = @"\gmManual\Samples\FromIdl"; <========= Select.DeployLocation = @"\Temp"; Execute.Compile(project : @"\gmManual\Samples\RDOtoNet\src\RDOToNET.vbp"); Execute.Analyse(); Execute.Output(status : "New", filename : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_std.bnd"); Execute.Author(); Execute.Storage(action : "Close"); } private static void RDOtoNet_mig() { Execute.Storage(action : "Create", identifier : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_mig"); Select.DevEnv = "VS2013"; Select.Dialect = Dialects.csh; Select.BuildFile = BuildFileStatus.on; Select.System = @"\gmManual\Samples\FromIdl"; Select.Target = @"\gmManual\Samples\RDOtoNet\proj\usr"; Select.DeployLocation = @"\Temp"; Execute.Compile(project : @"\gmManual\Samples\RDOtoNet\src\RDOToNET.vbp"); Execute.Analyse(); Execute.Output(status : "New", filename : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_mig.bnd"); Execute.Author(); Execute.Storage(action : "Close"); } }}
The two methods RDOtoNet_csh() and RDOtoNet_mig() are direct translations of the two gmPL translation scripts RDOtoNet_std.xml and RDOtoNet_mig.xml and produce the same translations as their corresponding gmPL script. As with the gmPL versions, the only substantive difference between the two scripts is the Target directory. The standard script uses the standard IDF for MSRDO20; while the migrated script uses the migrated IDF and associated gmSL code. In general, there is no need or advantage to completely abandon using gmSL when doing gmAPI based migrations. Large scale migrations will typically include both approaches. That said this sample will do the entire migration using the gmAPI
Adding a Few Migration Names
The first step in the gmSL migration was to copy the standard MSRDO20.DLL.XML IDF and to add migration names, using gmPL, to reflect any simple name changes that needed to be done. The approach here will be to copy method RDOtoNet_csh() as a new method RDOtoNet_api() and a new class msrdo that will perform the detailed migrations of the standard IDF. Here is the initial version of the new method RDOtoNet_api()
private static void RDOtoNet_api() { Execute.Storage(action : "Create", identifier : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_api"); Select.DevEnv = "VS2013"; Select.Dialect = Dialects.csh; Select.BuildFile = BuildFileStatus.on; Select.System = @"\gmManual\Samples\FromIdl"; Select.Target = @"\gmManual\Samples\FromIdl"; (1) Select.DeployLocation = @"\Temp"; Execute.Reference( id : "msrdo20.dll" ); (2) msrdo.StartAnalyser(99); (3) msrdo.AddMigrationNames(); (3) Execute.Compile(project : @"\gmManual\Samples\RDOtoNet\src\RDOToNET.vbp"); Execute.Analyse(); Execute.Output(status : "New", filename : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_api.bnd"); Execute.Author(); Execute.Storage(action : "Close"); }
For housekeeping purposes the names of the storage area and the output file are changed. Note first that it uses the standard IDF for MSRDO20.DLL. Notice second that the changes begin with the "Execute.Reference" call which pre loads that standard IDF. Since the added migration names include code patterns they must be made before the compiler runs; therefore, the IDF must be loaded explicitly. Third, the two calls to the msrdo class then make the changes. The initial code for this class is as follows.
namespace gmAPI { public class msrdo { private const int NCOMP = 14; private const int MSRDO20_RDO = 0; private const int RDO_rdoConnection_Connect = 1; private const int RDO_rdoConnection_EstablishConnection = 2; private const int RDO_rdoConnection_CreateQuery = 3; private const int RDO_rdoPreparedStatement_rdoParameters = 4; private const int RDO_rdoPreparedStatement_OpenResultset = 5; private const int RDO_rdoPreparedStatement_Close = 6; private const int RDO_rdoConnection = 7; private const int RDO_rdoResultset = 8; private const int RDO_rdoQuery = 9; private const int RDO_rdoResultset_EOF = 10; private const int RDO_rdoResultset_MoveNext = 11; private const int RDO_rdoResultset_rdoColumns = 12; private const int RDO_rdoColumn_Value = 13; private static string[] components = new string[NCOMP] { "RDO", "RDO._rdoConnection.Connect", "RDO._rdoConnection.EstablishConnection", "RDO._rdoConnection.CreateQuery", "RDO.rdoPreparedStatement.rdoParameters", "RDO.rdoPreparedStatement.OpenResultset", "RDO.rdoPreparedStatement.Close", "RDO.rdoConnection", "RDO.rdoResultset", "RDO.rdoQuery", "RDO._rdoResultset.EOF", "RDO._rdoResultset.MoveNext", "RDO._rdoResultset.rdoColumns", "RDO._rdoColumn.Value" }; private static int[] compAddr = new int[NCOMP]; private static int CreateCommand = 0; private static int Parameters = 0; private static int OpenResultset = 0; private static int CloseResultset = 0; public static void StartAnalyser(int migNumber) { Analyser.StartAnalyser(components,compAddr,migNumber); } public static void AddMigrationNames() { tMigInfo migInfo = null; migInfo = new tMigInfo(compAddr[MSRDO20_RDO], true); migInfo.migPattern = Symbol.StorePattern("System.Data.SqlClient",0); migInfo.LocalLibrary = true; Store.SetName(compAddr[RDO_rdoConnection_Connect], "ConnectionString"); migInfo = new tMigInfo(compAddr[RDO_rdoConnection_EstablishConnection], true); migInfo.migPattern = Symbol.StorePattern(@"%1d.Open()\c", 4); Store.SetName(compAddr[RDO_rdoConnection_CreateQuery], "CreateCommand"); CreateCommand = Symbol.StorePattern(@"(%1d = %2d()).CommandText = %4d\c", 4); Store.SetName(compAddr[RDO_rdoPreparedStatement_rdoParameters], "Parameters"); Parameters = Symbol.StorePattern(@"%1d.Add(new System.Data.SqlClient.SqlParameter(\S@%2d\S, null))", 2); Store.SetName(compAddr[RDO_rdoPreparedStatement_OpenResultset], "ExecuteReader"); OpenResultset = Symbol.StorePattern(@"using (System.Data.SqlClient.SqlDataReader %1d = %2d())\n{\p", 5); CloseResultset = Symbol.StorePattern(@"\q}\n", 1); Store.SetName(compAddr[RDO_rdoConnection], "SqlConnection"); Store.SetName(compAddr[RDO_rdoResultset], "SqlDataReader"); Store.SetName(compAddr[RDO_rdoQuery], "SqlCommand"); Store.SetName(compAddr[RDO_rdoResultset_EOF], "Read()"); } }}
The first part of every gmAPI based migration code consists of a list of the components to be explicitly referenced in the code along with their fully qualified identifiers. This list includes those components whose names and/or surface patterns are to be changed and any additional components whose references are to be transformed. The gmslLibrary method Analyser.StartAnalyser() then finds and stores the root offsets of these components and logs warning messages if any are not found. Using these root offsets the method AddMigrationNames() then makes the initial changes. Note that in addition to adding names and patterns to existing symbols in the IDF the changes to MSRDO20.dll.XML for the gmSL migrations also contain a new "DotNet" migclass.
<migclass id="DotNet"> <method id="CreateCommand" type="void" migPattern="(%1d = %2d()).CommandText = %4d\c"> <argument id="conn" type="Object" status="ByVal" /> <argument id="Name" type="String" status="ByVal"/> <argument id="SqlString" type="Variant" status="ByVal"/> </method> <Method id="Parameters" type="object" migPattern="%1d.Add(new System.Data.SqlClient.SqlParameter(\S@%2d\S, null))" > <argument id="index" type="Integer" status="ByVal" /> </Method> </migClass>
The purpose of this addition was to add new surface patterns that could then be referenced by the migration code. In this approach these patterns can simply be added directly using the Symbol.StorePattern method. With this machinery in place, the api translations are now as follows.
public static void connectDB() { conn = new System.Data.SqlClient.SqlConnection(); string dbs = ""; dbs = "UID=stocks_login;PWD=password;Database=stocks;" + "Server=GMI;Driver={SQL Server};" + "DSN='';"; conn.ConnectionString = dbs; conn.Open(); } public static void execQuery() { string SQL = ""; System.Data.SqlClient.SqlCommand SP = null; System.Data.SqlClient.SqlDataReader Results = null; string f = ""; SQL = "select * from accounts where accountID < ? and FirstName like ?"; SP = conn.CreateCommand("QueryAcct",SQL); SP.Parameters[0].Value = "5005"; SP.Parameters[1].Value = "Test%"; Results = SP.ExecuteReader(); while (!Results.Read()) { f = Results.rdoColumns["FirstName"].Value.ToStr(); writeLog(f); f = Results.rdoColumns["eMail"].Value.ToStr(); writeLog(f); Results.MoveNext(); } SP.Close(); }
This matchs the translation as of this step in the gmSL migration. To produce this new translation the method Translate.Main() is revised to produce three translations and three virtual binary information files.
RDOToNet_api.bnd RDOToNet_api.vbi RDOToNet_mig.bnd RDOToNet_mig.vbi RDOToNet_std.bnd RDOToNet_std.vbi
The goal of the remaining effort here is to produce an "api" bundle that matchs the gmSL produced "mig" bundle which is also matched by the current gmAPI translation RDOToNet_mig.bnd.
Starting the Migration Process
The first difference between the current translation and the target translation is in the method connectDB.
Current: dbs = "UID=stocks_login;PWD=password;Database=stocks;" + "Server=GMI-CS-01.gmi.local;Driver={SQL Server};" + "DSN='';"; Target : dbs = "UID=stocks_login;PWD=password;Database=stocks;Server=GMI-CS-01.gmi.local;";
The character string, which consists of a series of "name=value;" pairs has been changed to remove the Driver pair and the DSN pair.
The first question that has to be asked is "How do we determine which character strings need to be changed?". In an editing context, perhaps all strings could be searched for these pairs and they could be universally removed. This would almost certainly work in this case, but in general changes need to be contingent on their use. The string above needs to be changed because in this later statement
conn.ConnectionString = dbs;
it is being assigned to a connection string. Connection strings in the target environment do not have these two attributes. A search needs to be made of assignments to the ConnectionString property, and the strings used in these assignments need to be edited, if possible and necessary. It should be noted that this property has already been manipulated in the subtoptic "Adding a few migration names" where its target name was set to "ConnectionString". In that discussion its fully qualified source name was "RDO._rdoConnection.Connect" and its internal identifier within the msrdo class is "RDO_rdoConnection_Connect".
Migration is not a process that works with source code or target code. It works with the intermediate code produced by the compiler. After the compiler has completed and after the analyser has finished the standard migrations built into the tool, references in the intermediate code to components in external libraries can be scanned looking for those that have user supplied code registered for them. The purpose of the user code is to change the intermediate code so that it performs the target operation as opposed to its original source operation.
Once a component that requires migration has been identified, the first step is to locate references to it in the intermediate code. Going though this process here manually, first use the tool gmMetrics to produce a full audit report of the file "RDOToNet_api.vbi" produced above. The report itself is in a text file called "RDOToNet_api.txt". As a first step, search the "Audit of Symbol tree in RDOToNet_api.vbi" report for the fully qualified source name "RDO._rdoConnection.Connect".
Audit of Symbol tree in RDOToNet_api.vbi storage area: Lev | Address | Parent | Symbol Type | Full Symbol Identifier --- | ------- | ------ | ----------- | ---------------------- .... 4 | 56217 | 45315 | Lib_Property | RDO._rdoConnection.Connect
The important information here is the value "56217" in the "Address" column which contains the unique root offset of this component. The remainder of the audit report can now be searched for this value. Doing that uncovers a reference in the table
Detailed Description of Subprogram RDOToNET.connectDB with root address 101801: ... Actual csh Codeblock Associated with connectDB: Offset | Sl.Start | Ql.Start | Quantity type | Opcode | Operation support information ------ | -------- | -------- | ------------- | ------ | ----------------------------- ... 62 | | | | NEW | 41 conn.Connect = dbs 65 | | | | LEV | Nest0 67 | 1.67 | 1.67 | String | LDA | Variable:dbs:101851 72 | 1.67 | | | ARG | String 74 | 2.74 | 1.74 | RDO.SqlConnection | LDA | Variable:conn:101497 79 | 3.79 | 1.74 | String | LLP | Component:Connect:56217 84 | 2.74 | 1.74 | String | MEM | Child 86 | | | | STR | AssignValue
to the intermediate code for the conectionString assignment statement that we are interested in.
Starting the actual migration process, then, will involve writing a method in the msrdo class that will be notified of all code references to the Lib_PropertyRDO._rdoConnection.Connect. The purpose of that method will be to locate any strings being assigned to that property and to edit them to remove any unwanted attributes.
Introducing a Component Migration Method
The process of finding references to components so that those references can be migrated is exactly like the manual process described above. First, a list of the fully qualified names of the components is created and the root offsets of these components are recorded. This step was shown in the subtopic "Adding a Few Migration Names" and is performed via the statement "msrdo.StartAnalyser(99)" in the method RDOtoNet_api().
private static void RDOtoNet_api() { Execute.Storage(action : "Create", identifier : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_api"); Select.DevEnv = "VS2013"; Select.Dialect = Dialects.csh; Select.BuildFile = BuildFileStatus.on; Select.System = @"\gmManual\Samples\FromIdl"; Select.Target = @"\gmManual\Samples\FromIdl"; Select.DeployLocation = @"\Temp"; Execute.Reference( id : "msrdo20.dll" ); msrdo.StartAnalyser(99); msrdo.AddMigrationNames(); Execute.Compile(project : @"\gmManual\Samples\RDOtoNet\src\RDOToNET.vbp"); Execute.Analyse(); msrdo.CodeScan(99); Execute.Output(status : "New", filename : @"\gmManual\Samples\RDOtoNet\api\RDOToNet_api.bnd"); Execute.Author(); Execute.Storage(action : "Close"); }
The second step is performed via the statement "msrdo.CodeScan(99)" which scans the code for all the references to the root offsets derived via "StartAnalyser()" and then calls a separate method for each. Here is the source as added to the class msrdo.
private static int msrdo_Connect(int subRoot,int target,int iRefer,int[] qeStack) { FileSystem.LogMessage("RDO#01: sub<" + Symbol.FullName(subRoot,-1) + "> target=" + target + " iRefer=" + iRefer + " qeStack(" + qeStack[0] + ", " + qeStack[1] + ", " + qeStack[2] + ")" ); return 0; } private static int RDO_Transform(int subRoot,int target,int iRefer,int[] qeStack) { int icomp = 0; tMigInfo migInfo = null; migInfo = new tMigInfo(target); icomp = migInfo.migTransform % 256; switch(icomp) { case RDO_rdoConnection_Connect: return msrdo_Connect(subRoot,target,iRefer,qeStack); default: return 0; } } public static void CodeScan(int migNumber) { Analyser.Transform migrate = new Analyser.Transform(RDO_Transform); Analyser.CodeScan(migNumber,migrate); }
The code scan itself is performed via the method Analyser.CodeScan(). This method will call the method RDO_Transform() each time it encounters a component that is identified via the migNumber for msrdo which is 99 in this sample. The method RDO_Transform() will in turn extract the component identifier number and will call the appropriate method for each. So far the only method implemented is the msrdo_Connect() method called each time the RDO_rdoConnection_Connect component is referenced. At this point that method simply logs a message describing the reference.
All transform methods have four parameters. The parameter subRoot is the root of the source code component that references the component being transformed. The parameter target is the root offset of the component referenced. The parameter iRefer is the code location of the actual reference to the transform component. The parameter qeStack is a vector used internally to keep track of the starts of the code for the nested quantity expressions surrounding the reference to the target component. Its initial entry specifies the nesting level and its following members contain the starting offsets at each level. Its exact values will vary by the type of reference. Running the program now generates the following message.
RDO#01: sub<RDOToNET.connectDB> target=56217 iRefer=79 qeStack(2, 67, 74)
The actual message produced by the transform method, confirms what was obtained manually before which is repeated here.
Detailed Description of Subprogram RDOToNET.connectDB with root address 101801: ... Actual csh Codeblock Associated with connectDB: Offset | Sl.Start | Ql.Start | Quantity type | Opcode | Operation support information ------ | -------- | -------- | ------------- | ------ | ----------------------------- ... 62 | | | | NEW | 41 conn.Connect = dbs 65 | | | | LEV | Nest0 67 | 1.67 | 1.67 | String | LDA | Variable:dbs:101851 72 | 1.67 | | | ARG | String 74 | 2.74 | 1.74 | RDO.SqlConnection | LDA | Variable:conn:101497 79 | 3.79 | 1.74 | String | LLP | Component:Connect:56217 84 | 2.74 | 1.74 | String | MEM | Child 86 | | | | STR | AssignValue
The method connectDB contains a reference to RDO._rdoConnection.Connect whose unique root offset is 56217 at code offset 79 and that a good starting point for the overall reference is at offset 67 which in this case is the offset of the reference to the variable dbs whose content will eventually be evaluated and modified. Finally, the offset of the fully qualified reference to the property is at offset 74 which is the reference to the variable conn.
The msrdo_Connect Migration Code
The actual code used to ultimately change the content of the dbs connection string works through references to the RDO._rdoConnection.Connect component. The simple version of the method msrdo_Connect which simply logs a message is replaced by a method which does the migration. It is as follows.
private static int msrdo_Connect(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; string connect; int iPos; int semi; indexInfo = new CodeGroup(subRoot,target,iRefer); <=== (1) if(!indexInfo.isPropertySetter(qeStack)) return 0; <=== (2) if(!indexInfo.isString("rightSide")) return 0; <=== (3) connect = indexInfo.stringValue("rightSide"); <=== (4) if(connect == null) return 0; iPos = Character.FindFirst(connect,0,"Driver="); <=== (5) if(iPos != 0) { iPos--; semi = Character.FindFirst(connect,iPos,";"); if(semi != 0) { connect = Character.Remove(connect,iPos,semi); } } iPos = Character.FindFirst(connect,0,"DSN="); if(iPos != 0) { iPos--; semi = Character.FindFirst(connect,iPos,";"); if(semi != 0) { connect = Character.Remove(connect,iPos,semi); } } return indexInfo.stringReplace("rightSide",connect); <=== (6) }
The first thing to notice is the use of the gmslLibrary.CodeGroup class. This class generalizes the types of references and migrations that are performed when migrating external libraries. Particular migrations can then be done more easily. Step 1 then is to create an CodeGroup instance which can be manipulated. This particular migration deals with a value being set for the Connect property; therefore, if this particular reference is not a set to a property value then it is not to be migrated here. Step 2 then checks this. In addition to verifying that the reference is a property value set the method isPropertySetter also establishes the various code segments associated with the reference. In this case we are interested in the righSide segment which contains the code for the value being assigned. Step 3 verifies that the rightSide segment has a string type. If so, step 4 attempts to actually obtain that value. This may or may not be possible depending on the actual code. In this case though the actual assignment to the property is via a local variable, the logic in the CodeGroup class is able to find the string asigned to that variable; therefore, the actual transformation can proceed. The step 5 code simply changes the string value to remove the two unwanted name, value pairs. The final step 6 is to replace the old string expression with the revised string. Note that this replacement may well shift code that precedes the referencing code. The calling method that is scanning for transform references needs to be told that this has happened. A non-zero return value tells the scanner that the method has made a change in the code and that scanning should resume at the indicated code location.
Running this new code does now produce the desired change as the following file comparison shows.
Comparing files RDOToNet_api.bnd and RDOToNet_api.sav ***** RDOToNet_api.bnd dbs = "UID=stocks_login;PWD=password;Database=stocks;Server=GMI-CS-01.gmi.local;"; ***** RDOToNet_api.sav dbs = "UID=stocks_login;PWD=password;Database=stocks;" + "Server=GMI-CS-01.gmi.local;Driver={SQL Server};" + "DSN='';"; *****
The msrdo_CreateQuery Migration Code
The next difference between the unmigrated and migrated code is in execQuery in the string assigned to the variable SQL.
Current: SQL = "select * from accounts where accountID < ? and FirstName like ?"; Target : SQL = "select * from accounts where accountID < @0 and FirstName like @1";
As with the previous string change requirement the fact that this string should change is determined from the fact that the variable SQL is used as the second argument to the method RDO._rdoConnection.CreateQuery.
Set SP = conn.CreateQuery("QueryAcct", SQL)
Though the reference pattern is different and the actual editing is different, the logic of the migration method is about the same as the logic of the msrdo_Connect method. The name of the method is now constructed to refer to CreateQuery, the parameters are the same four.
private static int msrdo_CreateQuery(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; string query; int index; int iPos; int lPos; indexInfo = new CodeGroup(subRoot,target,iRefer); <=== (1) if(!indexInfo.isMethodCall(qeStack)) return 0; if(!indexInfo.isString("SqlString")) return 0; <=== (2) query = indexInfo.stringValue("SqlString"); if(query == null) return 0; iPos = 0; for(index = 0; index < 10; index = index + 1) { lPos = Character.FindFirst(query,iPos,"?"); if(lPos == 0) break; iPos = iPos + lPos - 1; query = Character.Remove(query,iPos,1); query = Character.Insert(query,iPos,"@" + index); } if(iPos != 0) indexInfo.stringReplace("SqlString",query); lPos = indexInfo.endsWithPattern("CUF,REF,ARG,CMD.Set"); if(lPos > 0) indexInfo.replaceEnding(lPos,CreateCommand,true,true); return 1; }
The first step is to make certain that this reference is a valid call to the method. The second step examines the "SqlString" code segment associated with the instance. For method calls the code segment names are derived from the definition of the method in its interface description file.
<method id="CreateQuery" type="rdoQuery"> <argument id="Name" type="String" status="ByVal"/> <argument id="SqlString" type="Variant" status="ByVal"/> </method>
Thus "SqlString" is the name of the code segment associated with the second parameter of the method call. Except for the name of the code segment the calls to the methods isString, stringValue, and stringReplace are the same as they were in the msrdo_Connect migration method.
Running this code to this point causes the desired change in the translation as the following file comparison shows.
***** RDOToNet_api.bnd SQL = "select * from accounts where accountID < @0 and FirstName like @1"; ***** RDOToNet_api.sav SQL = "select * from accounts where accountID < ? and FirstName like ?"; *****
In addition to having to change the content of the query string, the actual call to the method needs to be changed into a combined method call followed by a propery assignment. The actual difference is
Current: SP = conn.CreateCommand("QueryAcct",SQL); Target : (SP = conn.CreateCommand()).CommandText = SQL;
The easiest way to acheive these types of changes is to invent a new surface pattern that reflects the new desired target syntax. This added declaration was introduced as part of the AddMigrationNames method described earlier.
CreateCommand = Symbol.StorePattern(@"(%1d = %2d()).CommandText = %4d\c", 4);
Note that this pattern combines the code associated with the call to the method CreateCommand with its assignment to SP. The actual code here is this
51 | | | | NEW | 51 Set SP = conn.CreateQuery("QueryAcct", SQL) 54 | 1.54 | 1.54 | RDO.SqlCommand | LDA | Variable:SP:102053 59 | 1.54 | 1.54 | RDO.SqlCommand | LEV | Nest0 61 | 2.61 | 2.61 | RDO.SqlConnection | LDA | Variable:conn:101504 66 | 3.66 | 2.61 | RDO.SqlCommand | LLP | Component:CreateQuery:58213 71 | 2.61 | 2.61 | RDO.SqlCommand | MEM | Child 73 | 2.61 | 2.61 | RDO.SqlCommand | LEV | Nest1 75 | 3.75 | 3.75 | String | LSC | 9:QueryAcct 80 | 3.75 | 2.61 | RDO.SqlCommand | ARG | String 82 | 3.75 | 2.61 | RDO.SqlCommand | LEV | Nest1 84 | 4.84 | 3.84 | String | LDA | Variable:SQL:101992 89 | 4.84 | 2.61 | RDO.SqlCommand | ARG | String 91 | 2.61 | 2.61 | RDO.SqlCommand | CUF | Args2 93 | 2.61 | 2.61 | RDO.SqlCommand | REF | Component:CreateQuery:58213 98 | 2.61 | 1.54 | RDO.SqlCommand | ARG | Object 00 | | 2.100 | Void | CMD | Set
The CUF.Args2 operation marks the end of the code group associated with the isMethodCall instance. So the migration needs to know whether the instance ends in the operation sequence CUF,REF,ARG,CMD.Set and if so then that ending should instead be a reference to the surface pattern above. Operation sequences like the above are called "code patterns" and are processed via the CodePattern class and can be accessed via the CodeGroup instance. Here is the additional code needed.
lPos = indexInfo.endsWithPattern("CUF,REF,ARG,CMD.Set"); if(lPos > 0) indexInfo.replaceEnding(lPos,CreateCommand,true,true);
This code says exactly what is needed. If the instance ends in the specified code pattern then replace that ending with a reference to the CreateCommand surface pattern. Here is the final code.
51 | | | | NEW | 51 Set SP = conn.CreateQuery("QueryAcct", SQL) 54 | 1.54 | 1.54 | RDO.SqlCommand | LDA | Variable:SP:102053 59 | 1.54 | 1.54 | RDO.SqlCommand | LEV | Nest0 61 | 2.61 | 2.61 | RDO.SqlConnection | LDA | Variable:conn:101504 66 | 3.66 | 2.61 | RDO.SqlCommand | LLP | Component:CreateQuery:58213 71 | 2.61 | 2.61 | RDO.SqlCommand | MEM | Child 73 | 2.61 | 2.61 | RDO.SqlCommand | LEV | Nest1 75 | 3.75 | 3.75 | String | LSC | 9:QueryAcct 80 | 3.75 | 2.61 | RDO.SqlCommand | ARG | String 82 | 3.75 | 2.61 | RDO.SqlCommand | LEV | Nest1 84 | 4.84 | 3.84 | String | LDA | Variable:SQL:101992 89 | 4.84 | 2.61 | RDO.SqlCommand | ARG | String 91 | 4.84 | 1.54 | RDO.SqlCommand | ARG | Variant 93 | | 2.93 | String | APS | 4,(%1d = %2d()).CommandText = %4d\c
Note that the statement that uses the new surface pattern is highlighted. The comparison log shows that the code achieved the desired result.
***** RDOToNet_api.bnd (SP = conn.CreateCommand()).CommandText = SQL; ***** RDOToNet_api.sav SP = conn.CreateCommand("QueryAcct",SQL); *****
The msado_Parameters Migration Code
The next difference between the unmigrated and migrated code is as follows. This particular difference occurs twice.
Current: SP.Parameters[0].Value = "5005"; Target : SP.Parameters.Add(new System.Data.SqlClient.SqlParameter("@0", null)).Value = "5005";
The compiler processes this by generating a generic COL.Item operation that must be replaced by a pattern string.
Parameters = Symbol.StorePattern(@"%1d.Add(new System.Data.SqlClient.SqlParameter(\S@%2d\S, null))", 2);
The code produced by the tool before this migration is applied is as follows.
098 | | | | NEW | 52 SP.rdoParameters(0).Value = "5005" 101 | | | | LEV | Nest0 103 | 1.103 | 1.103 | String | LSC | 4:5005 108 | 1.103 | | | ARG | String 110 | 2.110 | 1.110 | RDO.SqlCommand | LDA | Variable:SP:102053 115 | 3.115 | 1.110 | RDO.rdoParameters | LLP | Component:rdoParameters:72062 120 | 2.110 | 1.110 | RDO.rdoParameters | MEM | Child 122 | 2.110 | 1.110 | RDO.rdoParameters | LEV | Nest0 124 | 3.124 | 2.124 | Integer | LIC | 0 127 | 3.124 | 1.110 | RDO.rdoParameters | ARG | Integer 129 | 2.110 | 1.110 | RDO.rdoParameter | COL | Item 131 | 3.131 | 1.110 | Variant | LLP | Component:Value:75147 136 | 2.110 | 1.110 | Variant | MEM | Child 138 | | | | STR | AssignValue
This method msrdo_Parameters performs the transformation.
private static int msrdo_Parameters(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; int lPos; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isPropertyGetter(qeStack)) return 0; lPos = indexInfo.endsWithPattern("LEV,LIC,ARG,COL.Item"); if(lPos == 0) return 0; indexInfo.replaceEnding(lPos,Parameters,false,false); return 1; }
First it verifies that the reference to RDO.rdoParameters is a property getter type operation. Second, it verifies that the reference is followed by a constant integer subscript associated with the property using the COL.Item operation. If so, it replaces that operation with a reference to the Parameters surface pattern. As the following change log shows, both instances of the rdoParameters where changed.
***** RDOToNet_api.bnd SP.Parameters.Add(new System.Data.SqlClient.SqlParameter("@0", null)).Value = "5005"; SP.Parameters.Add(new System.Data.SqlClient.SqlParameter("@1", null)).Value = "Test%"; ***** RDOToNet_api.sav SP.Parameters[0].Value = "5005"; SP.Parameters[1].Value = "Test%"; *****
The msado_OpenResultset and msrdo_Close Migration Codes
An important difference in the migrated code has to do with the SqlDataReader variable Results. In the unmigrated version, it is declared as a method level local variable and is then opened and closed in the code.
System.Data.SqlClient.SqlDataReader Results = null; ... Results = SP.ExecuteReader(); ... SP.Close();
In the migrated version, its scope is limited, as it is declared and opened in a using statement and then closed by closing the bracketed loop;
using (System.Data.SqlClient.SqlDataReader Results = SP.ExecuteReader()) { ... }
The two methods that transform the open and the close are shown here. They proceed in much the same manner as the ones already presented.
private static int msrdo_OpenResultset(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; int lPos; int setTarget; tVariable variable; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isMethodCall(qeStack)) return 0; lPos = indexInfo.endsWithPattern("CUF,REF,ARG,CMD.Set"); if(lPos == 0) return 0; setTarget = indexInfo.getSetTarget(); if(setTarget == 0) return 0; indexInfo.replaceEnding(lPos,OpenResultset,true,true); variable = new tVariable(setTarget,true); variable.DeadCode = true; return 1; } private static int msrdo_Close(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; int lPos; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isMethodCall(qeStack)) return 0; lPos = indexInfo.endsWithPattern("CUP"); if(lPos > 0) indexInfo.replaceEnding(lPos,CloseResultset,false,true); return 1; }
The only new CodeGroup method introduced is the getSetTarget method which retrieves the root offset of the left-hand-side of the set which is the variable Results. Setting the DeadCode property of this variable to true blocks its explicit declaration.
The msrdo_EOF Migration Code
The next difference is in the while loop that reads the records from the result set.
Current: while (!Results.Read()) Target : while (Results.Read())
In the current unmigrated code the while loop checks for an end-of-file while the target code performs the actual read. Note that using the types of techniques used earlier, the approach already changed the migName of the EOF property to Read() in the method AddMigrationNames.
Store.SetName(compAddr[RDO_rdoResultset_EOF], "Read()");
Then the migration method for the property can simply check for the NOT operation and remove it. This is what the actual reference code looks like.
266 | 2.266 | 1.261 | Boolean | LLP | Component:EOF:59440 271 | 1.261 | 1.261 | Boolean | MEM | Child 273 | 1.261 | 1.261 | Boolean | NOT | Arithmetic
The migration method then merely checks for the pattern and removes the NOT if present. The code is straight forward.
private static int msrdo_EOF(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; int lPos; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isPropertyGetter(qeStack)) return 0; lPos = indexInfo.endsWithPattern("NOT"); if(lPos == 0) return 0; indexInfo.replaceEnding(lPos,0,false,true); return 1; }
Since the surface pattern offset sent to the method replaceEnding is zero, the call results in the simple removal of the ending code. Checking the change log shows that the combination of the new migName and the removal of the NOT acheived the correct result.
***** RDOToNet_api.bnd while (Results.Read()) ***** RDOToNet_api.sav while (!Results.Read()) *****
Migrations that combine noncontingent renaming with contingent code modification are referred to as "shallow" migrations. The technology used by the tool is derived from the field of transformational grammar. The meaning of sentences is referred to as "deep structure", and the representation of the sentence as uttered is referred to as "surface structure". Rules that mix these two levels are called "shallow" and should generally be avoided. In our sample code, the only reference to the "EOF" property is in that while clause where the renaming to "Read()" is valid. But in other contexts, the transform would fail to apply, but the "EOF" would still be changed "Read()" -- certainly causing bad code.
In places such as this, shallow transforms are fine, but beware of them in larger scale migrations where they can introduce problems. A more complex approach would introduce a new surface pattern Read and then do a contingent replacement.
The msrdo_Columns and msrdo_ColumnValue Migration Code
The next difference is in the while loop that reads the records from the result set.
Current: f = Results.rdoColumns["FirstName"].ToStr(); Target : f = Results["FirstName"].ToStr();
This difference involves the references to two properties rdColumns and to Value both of which can simply be removed. The actual references can be seen in the code audit.
280 | | | | LEV | Nest0 282 | 1.282 | 1.282 | RDO.rdoResultset | LDA | Variable:Results:106065 287 | 2.287 | 1.282 | RDO.rdoColumns | LLP | Component:rdoColumns:57254 292 | 1.282 | 1.282 | RDO.rdoColumns | MEM | Child 294 | 1.282 | 1.282 | RDO.rdoColumns | LEV | Nest1 296 | 2.296 | 2.296 | String | LSC | 9:FirstName 301 | 2.296 | 1.282 | RDO.rdoColumns | ARG | String 303 | 1.282 | 1.282 | RDO.rdoColumn | COL | Item 305 | 2.305 | 1.282 | Variant | LLP | Component:Value:54311 310 | 1.282 | 1.282 | Variant | MEM | Child
Except for the names of the methods they are identical.
private static int msrdo_Columns(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isPropertyGetter(qeStack)) return 0; return indexInfo.argReplace("target",0); } private static int msrdo_ColumnValue(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isPropertyGetter(qeStack)) return 0; return indexInfo.argReplace("target",0); }
The calls to argReplace are the same as those to replaceEnding except that the code segment associated with the named argument of the instance is replaced. The zero surface pattern value simply performs a deletion of the named code segment. The change log shows that these changes produced the desired result.
***** RDOToNet_api.bnd f = Results["FirstName"].ToStr(); writeLog(f); f = Results["eMail"].ToStr(); ***** RDOToNet_api.sav f = Results.rdoColumns["FirstName"].Value.ToStr(); writeLog(f); f = Results.rdoColumns["eMail"].Value.ToStr(); *****
The msrdo_MoveNext Migration Code
The final difference between the two translations is that the MoveNext call is no longer needed. The migrated code already does the read in the while loop. The transform method can simply comment out the unwanted statement.
private static int msrdo_MoveNext(int subRoot,int target,int iRefer,int[] qeStack) { CodeGroup indexInfo; int lPos; indexInfo = new CodeGroup(subRoot,target,iRefer); if(!indexInfo.isMethodCall(qeStack)) return 0; lPos = indexInfo.endsWithPattern("CUP"); if(lPos == 0) return 0; indexInfo.commentOut(CMT.Delete); return 1; }
his demonstration migration has now been completed,