The FreezeScript tool transformdb migrates a database created by a Freeze map or evictor. It accomplishes this by comparing the "old" Slice definitions (i.e., the ones that describe the current contents of the database) with the "new" Slice definitions, and making whatever modifications are necessary to ensure that the transformed database is compatible with the new definitions.
This would be difficult to achieve by writing a custom transformation program because that program would require static knowledge of the old and new types, which frequently define many of the same symbols and would therefore prevent the program from being loaded. The
transformdb tool avoids this issue using an interpretive approach: the Slice definitions are parsed and used to drive the migration of the database records.
The default transformations performed by transformdb preserve as much information as possible. However, there are practical limits to the tool’s capabilities, since the only information it has is obtained by performing a comparison of the Slice definitions.
As the developers, we know that the int member has been renamed from
i to
j, but to
transformdb it appears that member
i was removed and member
j was added. The default transformation results in exactly that behavior: the value of
i is lost, and
j is initialized to a default value. If we need to preserve the value of
i and transfer it to
j, then we need to use custom migration (see
Section 37.3.5).
Changes in the type of a value are restricted to certain sets of compatible changes. This section describes the type changes supported by the default transformations. All incompatible type changes result in a warning indicating that the current value is being discarded and a default value for the new type assigned in its place. Additional flexibility is provided by custom migration, as described in
Section 37.3.5.
A value of type bool can be transformed to and from
string. The legal string values for a
bool value are
"true" and
"false".
The integer types byte,
short,
int, and
long can be transformed into each other, but only if the current value is within range of the new type. These integer types can also be transformed into
string.
The floating-point types float and
double can be transformed into each other, as well as to
string. No attempt is made to detect a loss of precision during transformation.
A string value can be transformed into any of the primitive types, as well as into enumeration and proxy types, but only if the value is a legal string representation of the new type. For example, the string value
"Pear" can be transformed into the enumeration
Fruit, but only if
Pear is an enumerator of
Fruit.
An enumeration can be transformed into an enumeration with the same type id, or into a string. Transformation between enumerations is performed symbolically. For example, consider our old type below:
enum Fruit { Apple, Orange, Pear };
Suppose the enumerator Pear is being transformed into the following new type:
enum Fruit { Apple, Pear };
The transformed value in the new enumeration is also Pear, despite the fact that
Pear has changed positions in the new type. However, if the old value had been
Orange, then the default transformation emits a warning because that enumerator no longer exists, and initializes the new value to
Apple (the default value).
A sequence can be transformed into another sequence type, even if the new sequence type does not have the same type id as the old type, but only if the element types are compatible. For example,
sequence<short> can be transformed into
sequence<int>, regardless of the names given to the sequence types.
A dictionary can be transformed into another dictionary type, even if the new dictionary type does not have the same type id as the old type, but only if the key and value types are compatible. For example,
dictionary<int, string> can be transformed into
dictionary<long, string>, regardless of the names given to the dictionary types.
Caution is required when changing the key type of a dictionary, because the default transformation of keys could result in duplication. For example, if the key type changes from
int to
short, any
int value outside the range of
short results in the key being initialized to a default value (namely zero). If zero is already used as a key in the dictionary, or another out-of-range key is encountered, then a duplication occurs. The transformation handles key duplication by removing the duplicate element from the transformed dictionary. (Custom migration can be useful in these situations if the default behavior is not acceptable.)
A struct type can only be transformed into another
struct type with the same type id. Data members are transformed as appropriate for their types.
A proxy value can be transformed into another proxy type, or into string. Transformation into another proxy type is done with the same semantics as in a language mapping: if the new type does not match the old type, then the new type must be a base type of the old type (that is, the proxy is widened).
A class type can only be transformed into another
class type with the same type id. A data member of a
class type is allowed to be widened to a base type. Data members are transformed as appropriate for their types. See
Section 37.5.10 for more information on transforming classes.
$ transformdb ‑‑old old/MyApp.ice ‑‑new new/MyApp.ice \
‑‑key int,string ‑‑value ::Employee db emp.db newdb
Briefly, the ‑‑old and
‑‑new options specify the old and new Slice definitions, respectively. These options can be specified as many times as necessary in order to load all of the relevant definitions. The
‑‑key option indicates that the database key is evolving from
int to
string. The
‑‑value option specifies that
::Employee is used as the database value type in both old and new type definitions, and therefore only needs to be specified once. Finally, we provide the pathname of the database environment directory (
db), the file name of the database (
emp.db), and the pathname of the database environment directory for the transformed database (
newdb).
See Section 37.5 for more information on using
transformdb.
Custom migration is useful when your types have changed in ways that make automatic migration difficult or impossible. It is also convenient to use custom migration when you have complex initialization requirements for new types or new data members, because custom migration enables you to perform many of the same tasks that would otherwise require you to write a throwaway program.
Custom migration operates in conjunction with automatic migration, allowing you to inject your own transformation rules at well-defined intercept points in the automatic migration process. These rules are called
transformation descriptors, and are written in XML.
We can use a simple example to demonstrate the utility of custom migration. Suppose our application uses a Freeze map whose type is
string and whose value is an enumeration, defined as follows:
enum BigThree { Ford, DaimlerChrysler, GeneralMotors };
We now wish to rename the enumerator DaimlerChrysler, as shown in our new definition:
enum BigThree { Ford, Daimler, GeneralMotors };
As explained in Section 37.3.2, the default transformation results in all occurrences of the
DaimlerChrysler enumerator being transformed into
Ford, because
Chrysler no longer exists in the new definition and therefore the default value
Ford is used instead.
<transformdb>
<database key="string" value="::BigThree">
<record>
<if test="oldvalue == ::Old::DaimlerChrysler>
<set target="newvalue"
value="::New::Daimler"/>
</if>
</record>
</database>
</transformdb>
When executed, these descriptors convert occurrences of DaimlerChrysler in the old type system into
Daimler in the transformed database’s new type system. Transformation descriptors are described in detail in
Section 37.4.