Can’t define abstract or final methods on abstract table

It seems Ax doesn’t allow you to use abstract or final on abstract tables. When adding abstract public myMethod() to an abstract table (e.g. EcoResProduct), you get this compile error:

Conflicting access modifier given.

The error message doesn’t really make sense because the only access modifier specified is public. I’m not sure if this is intended behaviour or an issue with the compiler. There’s a whitepaper that mentions you can’t declare a table as final but it doesn’t say anything about the methods. In any case, a more meaningful error message would help.

This was discovered on Ax 2012 R2 CU7 but we could reproduce it in the Ax 2012 R3 CU8 demo machine.

To enforce overrides on derived tables you can still use the old school method like so:

public void myMethod()
{
    throw error(Error::missingOverride(funcName()));
}

Inconsistent behavior between X++ and CIL using tables with inheritance

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
    }

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();
}

I then called the method in 2 ways:

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();
}

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)));

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.

Intrinsic function weirdness

Over at Dynamics Ax Daily I found a post about a compile error using tableNum() in a select statement.

As mentioned there, the following code doesn’t compile.

    select extCodeTable
        where extCodeTable.ExtCodeTableId == tableNum(CompanyInfo)
    join extCodeValueTable
        where extCodeValueTable.ExtCodeId == extCodeTable.ExtCodeId;

The compiler chokes on the call to tableNum(). I was surprised to see this, as I could have sworn that I have used tableNum() in select statements before. As it turns out it does compile in some cases.

It compiles without the join.

    select extCodeTable
        where extCodeTable.ExtCodeTableId == tableNum(CompanyInfo);

It also works if you add another where clause after tableNum().

    select extCodeTable
        where extCodeTable.ExtCodeTableId == tableNum(CompanyInfo)
             && extCodeTable.RecId != 0
    join extCodeValueTable
        where extCodeValueTable.ExtCodeId == extCodeTable.ExtCodeId;

And using a non-intrinsic function works too.

    select extCodeTable
        where extCodeTable.ExtCodeTableId == str2int("1234")
    join extCodeValueTable
        where extCodeValueTable.ExtCodeId == extCodeTable.ExtCodeId;

This example doesn’t make much sense with regards to business logic but it does compile.

I’m guessing this is a bug in the compiler. As far as I can tell it only fails when you use an intrinsic function before a join clause. Until it’s fixed just use a variable or throw in another where clause to check for RecId != 0. Since all records have a RecId it won’t affect the results you get back. This happens in Ax 4.0 and 2009.

All intrinsic functions are listed on MSDN.