Fixing AX SQLDICTIONARY table and field IDs
I set up my blog a while back, and this will be my first real post to it. My intent was that a major purpose of the blog would be to contribute back to the AX community. Many smart people's posts have saved me a lot of time over the years, and it's time to pay it forward. Unsurprisingly I suppose, it has taken me a while to get around to it, but hopefully I'll be able to establish some momentum, starting with this article.
Now that all the throat-clearing is out of the way, let's get to it.
One issue I keep running into in my AX 2012 consulting work is element ID conflicts. These happen for various reasons, including people not following best-practices for promoting code. I will not be discussing code management much in this post. There are lot's of good articles about what one should do on TechNet, and elsewhere, so perhaps I will save some of my own thoughts on the subject for a future article.
Similarly you may need to move code from one layer to another. The only "approved" method I am aware of is to export the objects (to an XPO) form the source layer, delete them, and then re-import them into the target layer. This results in new element IDs for any elements which did not already exist in a lower code layer. Janet Blake, for one, has posted about this procedure and the effect on element IDs. See here.
Suffice it to say that if you have for any reason created a new element in an AX instance other than your build instance, any later need to import a model store from your proper build instance (where you have also created the same element) exposes you to an element ID conflict. This is because in AX 2012, element IDs are assigned by the instance in which you create them. The same element created in two different instances will very likely have a different ID in each. This applies whether you created the element manually, via XPO import or via model import.
When you try to import a model store with a conflicting element ID, the import will abort unless you provide the /idconflict:overwrite parameter to AXUtil.exe (or the PowerShell Import-AXModelStore cmdlet equivalent). The question is what are the consequences when, as you need to, you go ahead on overwrite the conflicting target IDs. For many element types, the answer is really "nothing". However, there are some important exceptions.
One is data elements, i.e. tables and fields within tables: While you may now have successfully imported your model store, usually your next step is to do a SQL database synchronization, and this will result in AX issuing SQL DDL statements to try and make the physical schema of the AX business database conform to the metadata in your model store. It does this with reference to the SQLDICTIONARY table, which contains the names and IDs of the tables and fields. The usual outcome is SQL responding with errors about the ALTER TABLE statements attempting to change the name and type of a column to an incompatible data type, or worse, it is possible that compatible columns will be silently renamed, and end up containing nonsensical data.
This can be fixed by dropping the SQL table prior to synchronization, which causes them to be recreated, with the same table and columns names as before, and SQLDICTIONARY also gets updated with the new table and field IDs from the model store metadata. It’s worth noting that the end result is the same SQL schema as before the synchronization, possibly with the columns in a different order. Of course, before you drop a table, if it doesn’t happen to be empty already, you need to preserve the data, and restore it after the table is recreated. This is not actually that hard to do with a bit of T-SQL, although it can be a little nerve-wracking.
There is a simpler answer however: As we noted, the synchronization results in the same SQL tables containing the same columns. So, one can reframe the problem as SQLDICTIONARY being out of sync with the model store metadata. If you fix the table and field IDs to match the metadata prior to the database synchronization, it determines that the SQL tables and fields are already correct, and does not issue DDL statements to change them. I certainly can not take credit for coming up with this more elegant approach, and would like to refer you to Martin Drab’s blog post on the subject (here).
In addition to doing a great job of explaining this, he offers some X++ code to help. I’m sure like many others, I started using his code some time ago, and have since rewritten it to deal better with various scenarios I have run into. I have now used it quite a number of times, and it has proven useful and reliable on many occasions. I have included my updated version below. Please feel free to use it however you wish, understanding that I offer no warranty express or implied on the code, and make no claims as to its fitness for your particular purpose. I ask only that you leave the attributions to Martin and myself in place, and that you in turn share any brilliant improvements you may come up with.
Before I get to the code, allow me to point out a significant caveat. While it should allow you to get past your database synchronization challenges, if you run it before the synchronization, it makes no attempt to update any business data which may contain an affected element ID. There are not very many business data table fields containing element IDs that I am aware of, but you have to be on the lookout for them and take the appropriate steps to correct the data if you are unlucky enough to run into this. Two examples I will provide for now are financial dimension values (based on existing tables, not those configured to use values from “< Custom dimension >”) and security role assignments. The astute amongst you will realize right away that the latter has nothing at all to with database synchronization, but the principle is the same: If you have created a new role in two different instances, they will probably have different IDs. The role assignments are based on the IDs, so after importing a model store with ID conflicts you may find that people are assigned to an unexpected or non-existent role. In a future post I will share a T-SQL script which will correct this situation if provided with a restored copy of the original model store database to refer to.
A second note is that after running the job, it is still advisable to run a database synchronization, although it will not need to create or change any tables or fields. I recommend this as both a good-practice sanity check, and to ensure synchronization for any metadata differences with the SQL schema other than the tables themselves and their columns. I have also found that you will sometimes have to synchronize twice before it stops complaining about an index it is trying to create, but which already exists.
I salute you if you have made it all the way through what has become quite long post. Thank you for reading it, and here is the code. Please read the comments, noting particularly the potential need to temporarily drop and later re-create the I_65518FIELD index on SQLDICTIONARY. I welcome any questions or comments.
Martin Walker (mwalker@syndaxis.com), February 12th, 2015
// Note: You may have to drop and then later re-create the I_65518FIELD
// constraint on the SqlDictionary table to do this successfully.
// Adapted from concept by Martin Drab, replacing the releaseUpdateDB calls
// with the equivalent logic, and preventing incorrect updates to records where the
// field IDs are temporarily duplicated, e.g. if they need to be swapped.
//
// v2: Also now avoids updating records with shadow != 0, since views sometimes
// legitimately seem to have a duplicate field name with a different id and shadow = 1.
// v3: Adds support for scenarios where table IDs need to be swapped.
// v4: Adds support for table inheritance.
//
// Written by Martin Walker (mwalker@syndaxis.com)
static void MW_FixSQLDictTableAndFieldIDs(Args _args)
{
Dictionary dictionary = new Dictionary();
SysDictTable dictTable;
DictField dictField;
TableId tableId, tempTableId, rootTableId;
FieldId fieldId;
SqlDictionary sqlDictionaryTable;
SqlDictionary sqlDictionaryField;
SqlDictionary sqlDictionary;
SqlDictionary sqlField;
Map tableIdToFromMap = new Map(Types::Integer, Types::Integer);
MapEnumerator mapEnum;
List newTableIdList = new List(Types::Integer);
ListEnumerator listEnum;
TableName tableName,rootTableName;
setPrefix("Update of data dictionary IDs");
ttsBegin;
// First find the maximum table ID in use, and start temporary IDs one higher
select maxOf(TabId) from sqlDictionary
where sqlDictionary.fieldId == 0;
tempTableId = sqlDictionary.TabId + 1;
// Iterate through the tables and fix SQLDICTIONARY.TABLEID
tableId = dictionary.tableNext(0);
while (tableId)
{
dictTable = new SysDictTable(tableId);
if (!dictTable.isSystemTable())
{
// Finds table in SqlDictionary by name in AOT, if ID was changed.
// 0 field ID means a table.
select firstOnly sqlDictionaryTable
where sqlDictionaryTable.name == dictTable.name()
&& sqlDictionaryTable.fieldId == 0
&& sqlDictionaryTable.tabId != dictTable.id();
if (sqlDictionaryTable)
{
// Check whether target id already exists
select firstonly sqlDictionary
where sqlDictionary.TabId == dictTable.id()
&& sqlDictionary.FieldId == 0;
if (!sqlDictionary)
{
// Store new (key) and old (value) table IDs in map
tableIdToFromMap.insert(dictTable.id(),sqlDictionaryTable.tabId);
}
else
{
// First store a temporary new (key) and old (value) table ID in map
tableIdToFromMap.insert(tempTableId,sqlDictionaryTable.tabId);
// Store a second record in the map to change the temporary ID to the final value
tableIdToFromMap.insert(dictTable.id(),tempTableId);
tempTableId++;
}
}
}
tableId = dictionary.tableNext(tableId);
}
// Create list with new table IDs in _descending_ order
mapEnum = tableIdToFromMap.getEnumerator(); //Note: Enumerates in ascending key order
while (mapEnum.moveNext())
{
newTableIdList.addStart(mapEnum.currentKey());
}
// Now enumerate the list and update the table IDs
listEnum = newTableIdList.getEnumerator();
while (listEnum.moveNext())
{
tableId = listEnum.current();
dictTable = new SysDictTable(tableId);
// Where we have used a temporary table ID, dictTable cannot be used
if (dictTable)
tableName = dictTable.name();
else
tableName = "";
select firstOnly sqlDictionaryTable
where sqlDictionaryTable.tabId == tableId
&& sqlDictionaryTable.fieldId == 0;
if (sqlDictionaryTable)
{
// We dealt with this by first changing to a dummy table ID
// and then from the dummy ID to the desired ID
// so this should no longer happen, but just in case...
warning(strFmt("Unable to change Table %1 ID from %2 to %3 - target ID already in use",
tableName,
tableIdToFromMap.lookup(tableId),
tableId));
}
else
{
// Note that this must fix the table ID in the records for the table and its fields
// (which is why we can't just add the table name to the where clause
// to cover the scenario where the target table ID is in use).
// The field IDs will be corrected later in the job if necessary.
update_recordset sqlDictionary
setting TabId = tableId
where sqlDictionary.TabId == tableIdToFromMap.lookup(tableId);
info(strFmt("Table %1 ID changed from %2 to %3",
tableName,
tableIdToFromMap.lookup(tableId),
tableId));
}
}
// Now iterate through the tables again and fix SQLDICTIONARY.FIELDID
tableId = dictionary.tableNext(0);
while (tableId)
{
dictTable = new SysDictTable(tableId);
setPrefix(dictTable.name());
if (!dictTable.isSystemTable())
{
//With table inheritance, the SqlDictionary records have the root table id.
rootTableId = SysDictTable::getRootTable(tableId);
rootTableName = new SysDictTable(rootTableId).name();
select firstonly sqlDictionary
where sqlDictionary.tabId == rootTableId
&& sqlDictionary.fieldId == 0
&& sqlDictionary.name == rootTableName;
if (sqlDictionary)
{
fieldId = dictTable.fieldNext(0);
// For all fields in table
while (fieldId)
{
dictField = dictTable.fieldObject(fieldId);
if (dictField.isSql() && !dictField.isSystem())
{
// Finds fields in SqlDictionary by name and compares IDs
select firstOnly sqlDictionaryField
where sqlDictionaryField.tabId == rootTableId
&& sqlDictionaryField.name == dictField.name()
&& sqlDictionaryField.fieldId != 0
&& sqlDictionaryField.fieldId != dictField.id()
&& sqlDictionaryField.shadow == 0;
if (sqlDictionaryField && dictField.id() != 0)
{
// Updates field ID in SqlDictionary
update_recordset sqlField
setting FieldId = dictField.id()
where sqlField.TabId == rootTableId
&& sqlField.FieldId == sqlDictionaryField.fieldId
&& sqlField.Name == dictField.name()
&& sqlField.shadow == 0;
info(strFmt("Field %1 - ID changed (%2 -> %3)",
dictField.name(),
sqlDictionaryField.fieldId,
dictField.id()));
}
}
fieldId = dictTable.fieldNext(fieldId);
}
}
}
tableId = dictionary.tableNext(tableId);
}
ttsCommit;
}
|
|