This one is a doozy. Recently a customer contacted me because they were having problems running a job they made themselves to export product information. It seemed to work fine in the client during development but the results when running in batch were very different.
Starting from a query on EcoResProduct
they collect a lot of information and export it to a file which is picked up by another application. Sounds simple enough. It certainly is a lot easier than putting a fridge on a comet.
What they were trying to do came down to this:
query = new Query();
qbds = query.addDataSource(tableNum(EcoResProduct));
queryRun = new QueryRun(query);
while (queryRun.next())
{
ecoResProduct = queryRun.get(tableNum(EcoResProduct));
// Look up information in related tables, sometimes using table ID
} |
query = new Query();
qbds = query.addDataSource(tableNum(EcoResProduct));
queryRun = new QueryRun(query);
while (queryRun.next())
{
ecoResProduct = queryRun.get(tableNum(EcoResProduct));
// Look up information in related tables, sometimes using table ID
}
Nothing extraordinary going on there. Until they put it in batch. As it turns out ecoResProduct.TableId
was no longer correct and because of that they couldn’t find some related records.
While debugging I noticed that when running in batch the variable ecoResProduct
was actually of type EcoResDistinctProduct
. I could see it happening in Visual Studio. When running the job in my client and using the X++ debugger the variable was of type EcoResProduct
.
Then it clicked for me: it looks like the CIL version of the code handles abstract base tables differently. It makes sense actually, because you can’t create instances of abstract types, only of derived concrete types. In X++ it is somehow supported for instances of table buffers to be abstract types. I decided to test my hypothesis with another infamous table using inheritance: DirPartyTable
. I made a small class with a simple method to run a query on the table and get some records from it.
static container doStuff(container _params = conNull())
{
DirPartyTable dirPartyTable;
QueryRun queryRun;
Query query;
QueryBuildDataSource qbds;
TableId tableId = tableNum(DirPartyTable);
int i;
;
query = new Query();
qbds = query.addDataSource(tableId);
queryRun = new QueryRun(query);
while (queryRun.next())
{
dirPartyTable = queryRun.get(tableId);
info(strFmt('Expected ID: %1, Real ID: %2',
tableId, dirPartyTable.TableId));
++i;
if (i > 5)
break;
}
return conNull();
} |
static container doStuff(container _params = conNull())
{
DirPartyTable dirPartyTable;
QueryRun queryRun;
Query query;
QueryBuildDataSource qbds;
TableId tableId = tableNum(DirPartyTable);
int i;
;
query = new Query();
qbds = query.addDataSource(tableId);
queryRun = new QueryRun(query);
while (queryRun.next())
{
dirPartyTable = queryRun.get(tableId);
info(strFmt('Expected ID: %1, Real ID: %2',
tableId, dirPartyTable.TableId));
++i;
if (i > 5)
break;
}
return conNull();
}
I then called the method in 2 ways:
server static private void runXpp()
{
setPrefix(funcName());
TestQueryTableId::doStuff();
} |
server static private void runXpp()
{
setPrefix(funcName());
TestQueryTableId::doStuff();
}
server static private void runCIL()
{
container ret;
XppILExecutePermission xppILExecutePermission;
;
setPrefix(funcName());
xppILExecutePermission = new XppILExecutePermission();
xppILExecutePermission.assert();
ret = runClassMethodIL(classStr(TestQueryTableId),
staticMethodStr(TestQueryTableId, doStuff),
conNull());
CodeAccessPermission::revertAssert();
} |
server static private void runCIL()
{
container ret;
XppILExecutePermission xppILExecutePermission;
;
setPrefix(funcName());
xppILExecutePermission = new XppILExecutePermission();
xppILExecutePermission.assert();
ret = runClassMethodIL(classStr(TestQueryTableId),
staticMethodStr(TestQueryTableId, doStuff),
conNull());
CodeAccessPermission::revertAssert();
}
The results were exactly what I expected.
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2377
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2975
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2975
Luckily it can be fixed with SysDictTable::getRootTable()
.
info(strFmt('Expected ID: %1, Real ID: %2, Fixed ID: %3',
tableId, dirPartyTable.TableId, SysDictTable::getRootTable(dirPartyTable.TableId))); |
info(strFmt('Expected ID: %1, Real ID: %2, Fixed ID: %3',
tableId, dirPartyTable.TableId, SysDictTable::getRootTable(dirPartyTable.TableId)));
Giving me this:
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runXpp Expected ID: 2303, Real ID: 2303, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2377, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2978, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2975, Fixed ID: 2303
Info TestQueryTableId.run\TestQueryTableId::runCIL Expected ID: 2303, Real ID: 2975, Fixed ID: 2303
The problem was discovered in Ax 2012 R2 but I ran the test on and R3 demo machine and the results were exactly the same.
Quite confusing and time consuming to figure out what’s going on.