Contents
What is the Updater?
The Updater for xTuple ERP (PostBooks, Standard Edition, and OpenMFG Edition) is an application which enables you to update your databases from one release to the next, to upgrade from one xTuple ERP edition to another, such as from PostBooks to OpenMFG, and to load supplements developed by third parties into your xTuple database. The Updater reads and processes upgrade scripts or packages, which are collections of files bundled together into .gz files. This page documents how to create update packages.
Here are some other related documents:
If you want to know how to use the Updater to upgrade a database or install an add-on, look at UpdaterDoc.
If you are having problems running an upgrade, such as failures during the prerequisite checking, look at DebuggingUpgradeErrors.
What is a Package?
A Package is a compressed collection of files which may contain either a) custom software designed to extend the functionality of xTuple ERP without touching the core xTuple ERP source code or b) upgrade bundles designed to migrate xTuple ERP databases from one version to the next. xTuple creates packages to upgrade databases from one release to the next and to upgrade from one edition to another, such as from PostBooks to OpenMFG. Using Packages, third-party software developers can add features without needing C++ programming skills. And these features can be integrated seamlessly into xTuple ERP independent of the release cycles and source code requirements defined by xTuple. In short, Packages are flexible. And they make it easy for software developers to add new features--or even just simple tweaks--to customize and enhance the xTuple ERP experience.
Here are some other related documents:
To learn more about third-party offerings currently available for xTuple ERP, look at ThirdPartySolutions.
The structure of an Updater Package
At the most basic level, Updater Packages are compressed tar files. This format has been adopted as one of several standards for bundling files for distribution over the Internet.
The contents of the tar file have a fairly uniform structure. At the top level is a single directory. Inside this directory there must be a file called either contents.xml (for Updater versions 1.x) or package.xml (for Updater versions 2.x). This file describes to the Updater how to load the rest of the package. The other files included in the package are either SQL scripts to run to modify the database or any of the following items to load into the database:
- user interface forms
- application scripts
- images
- report definitions
- MetaSQL statements
These may all be directly in the top level directory of the package or arranged in subdirectories.
Getting started building an Updater Package
The first thing you need to do when building an Updater package is to create a directory:
$ mkdir sampledir
All of the command line examples in this document assume that you are working in a UNIX-like environment. That could be a Terminal window on the Macintosh, a Shell or Console window on Linux, or a Cygwin window on Windows. Things that the user types will always be preceded by '$ ', indicating a shell prompt. Lines without this prompt are sample output lines.
Everything that you want to include in this package should eventually be put into this directory. The first thing you should put in this directory is a package.xml file. Building a package is an iterative process of adding files to the directory, modifying package.xml, bundling everything together, and testing. So don't worry yet about having all of the pieces together.
package.xml
The package.xml file contains the information required to orchestrate a database upgrade or package addition to an xTuple database. The syntax is described in full below. The file is, as its name suggests, an XML file. The root element must be <package>. Attributes of the <package> element name the package, provide a consistency check with the package's top-level directory, and let the creator specify a minimum Updater version to use when loading the package:
<package id="sampledir"
name="samplepackage"
developer="xTuple"
descrip="Sample described on CreateUpdaterPackages wiki page."
updater="2.0.0">
<pkgnotes>
This is an example for the wiki page describing the
packaging functionality in xTuple ERP's Updater utility.
</pkgnotes>
...
</package>In order of appearance, here's what this all means:
The id attribute says that the contents of this package all come from a directory named sampledir.
By providing a name, the author is telling the Updater that this is an add-on package, rather than an update to the core xTuple ERP application. The package will be stored with the name samplepackage.
This package was developed by the company xTuple.
If this is an add-on package, the short description of this package displayed on the Package window in the xTuple ERP application will be as shown: Sample described on CreateupdaterPackages wiki page.
This package cannot be loaded by any version of the Updater before 2.0.0. This means that this package cannot be loaded by Updater 1.0, 1.1, or even 2.0.0BETA2. It can be loaded by Updater 2.0.0, 2.2.0ALPHA, or 3.2.7 (not that versions 2.2.0ALPHA or 3.2.7 exist yet:-)).
The <pkgnotes> are stored as comments for add-on packages and are visible on the Package window in the xTuple ERP window.
The file contents.xml, used by Updater versions 1.x to describe package contents, has been deprecated. Please do not create new packages using it.
Prerequisite Checks
The id and updater attributes of the <package> element allow for simplistic internal consistency checking: Does this package contain the expected directory and are you running a version of the Updater that can load this package. This is rarely sufficient for loading a package of any real complexity.
The Updater lets you provide database queries to make more interesting checks. xTuple uses these Query prerequisites extensively in upgrade packages to confirm that the starting database version for the upgrade matches expectations and to check for data that need to be fixed before the upgrade will be successful. The sole criterion for writing a correct prerequisite query is that it return an SQL boolean TRUE value if it is safe for the Updater to load this package.
Here is an example:
...
<prerequisite type="Query" name="Checking SQL DB Server" >
<query>SELECT metric_value = 'PostBooks'
FROM metric
WHERE metric_name = 'Application';</query>
<message>
This package requires that it be applied against a PostBooks database.
</message>
</prerequisite>
...The type attributes says that this <prerequisite> element is a database query.
Before the Updater runs the statement in the <query> element it writes the value of the name attribute, "Checking SQL DB Server", to the Updater's message window.
The <query> element then gets run, checking the metric table to see if this database is specifically a PostBooks database. If the database is an xTuple ERP Standard or OpenMFG Edition database, or if the database does not have a metric table, then the prerequisite check will fail.
If the query fails then the contents of the <message> element will be written to the Updater's message window, probably in red type.
All of the prerequisites are checked before the Updater returns from this phase of processing. This lets the user learn about all of the problems that s/he should fix before trying to run the Updater again to load this package.
There is also a license prerequisite, which allows the package developer to present the user with an end-user license agreement. The user must click an Accept button before the Updater will allow the package to be installed. This license is not written anywhere by the Updater. The license text in the <message> element may contain rich text, as defined by Trolltech:
...
<prerequisite type="license" name="sampleLicense" >
<message>
<p>This is the content of a <i>rich text</i> license.
<hr/>
You must either accept or not.</p>
</message>
</prerequisite>
...Notice the < in several places. Because this is an XML file, the < character used to mark the beginnings of rich text tags must be escaped so they are not treated as part of the <package> element tree. xTuple has tested the Updater using < substitution for <. Using XML CDATA sections should also work but xTuple has not tested this.
Most things are described in external files
Now that the preliminaries have been dispensed with, how do we actually load things into the database? For the most part by creating small files that do (or contain) just one thing.
You can create arbitrary database scripts. If you have several changes that you want to make to the database that are all related, they should go in a single script file. If there are several unrelated changes, create several different files. Put these script files into the package directory and add a line to package.xml for each:
... <script file="runFirst.sql" /> <script file="runSecond.sql" /> ...
The Updater has several different elements for specific kinds of database object. These can be used for either update or add-on packages; the syntax was specifically designed to support the bookkeeping necessary for add-on packages but may also be used in database upgrade or xTuple ERP edition upgrade packages. Put a script file for each of the following sample lines into the package directory:
... <createtable file="pkgtest.sql" name="pkgtest" /> <createfunction file="pkgtesttriggerfunction.sql" name="_pkgtestbefore" /> <createtrigger file="pkgtesttrigger.sql" name="pkgtestbefore" /> <createview file="pkgtestview.sql" name="pkgtestview" /> ...
Run the SQL script in the file called pkgtest.sql and, if this is an add-on package, record that a table named pkgtest is part of this package.
Run the SQL script in pkgtesttriggerfunction.sql and, if this is an add-on package, record that one or more functions named _pkgtestbefore are part of this package.
Run the SQL script in pkgtesttrigger.sql and, if this is an add-on package, record that there is a pkgtestbefore trigger in this package.
Run the SQL script in pkgtestview.sql and, if this is an add-on package, record that there is a view pkgtestview in this package.
The package creator has to decide whether to use the specific <createstuff> elements or <script>. If the package is a non-core feature contribution outside the application core, then the decision is easy to make: Use <createstuff> because these elements automatically do the proper bookkeeping for storing the package as a unit in the database and reporting the package contents accurately. When building upgrades to the core, however, the choice is less clear. One of the properties of the <createstuff> elements is that the Updater processes them in a particular order to minimize conflict. If this default order does not work for this particular upgrade, however, the package creator must use <script> elements to enforce a sequence that will work.
The Updater also has different elements for xTuple ERP objects. These are stored in special tables used by the xTuple ERP database. The files used for the following elements are not database scripts. Instead they hold the actual objects themselves.
... <loadmetasql file="sampleQuery.mql" /> <loadreport file="sampleReport.xml" /> <loadappscript file="sampleScreen.script" name="sampleScreen"/> <loadappui file="sampleScreen.ui" /> <loadimage file="sampleImage.png" name="aPicture" /> ...
Read a MetaSQL statement from the file sampleQuery.mql and save it in the database. See below for more information.
Read an OpenRPT report definition from the file sampleReport.xml and save it in the database.
Read an application script from sampleScreen.script and save it in the database with the name sampleScreen. This name allows it to work nicely with the next item in the package.
Read an application user interface definition from sampleScreen.ui and save it in the database.
Read an image from the file sampleImage.png and save it in the database as an image with the application-level name aPicture.
A few things are described in the file itself
You could create new privileges and custom commands using <script> elements. However, these things are so simple and so common that the Updater has direct support for creating them without SQL scripts.
To create a new privilege:
... <loadpriv name="ViewSampleScreen" module="System"> Allowed to view the sample screen </loadpriv> ...
This creates a privilege named ViewSampleScreen. It can be assigned to users or groups of users by selecting System from the Module combo box on the User Information or Group windows.
To create a new custom command:
...
<loadcmd name="SampleCMD" title="Sample Screen"
privname="ViewSampleScreen" module="System"
executable="!customuiform">
Sample Screen Custom Command Description - view mode
<arg value="uiform=SampleScreen"/>
<arg value="mode=view"/>
</loadcmd>
...This creates a new custom command called SampleCMD, which appears in the menu system as System -> Custom Commands -> Sample Screen. When the user selects this menu item, the application opens the User Interface form Sample Screen, which was loaded earlier, and passes it the argument mode=view.
Creating the Package itself
After you have copied all of the scripts, .ui files, .mql files, OpenRPT .xml files, etc. to your package directory, you can bundle all of the pieces together with a single command:
tar czf packagedir.gz packagedir
Packaging for xTuple ERP releases
This section documents the process xTuple uses to build an Updater package for new releases of xTuple ERP, specifically for PostBooks. The process is similar for the xTuple ERP Standard and OpenMFG Editions, but the packages for these two editions require data not available in the SourceForge repositories.
The xtupleserver portion of the SVN repository has three subdirectories:
dbscripts holds view, function, MetaSQL, trigger, and PostgreSQL type definitions.
updatescripts contains directories for each incremental release, including all alpha releases, all beta releases, all release candidates, etc.; each of these directories in turn has a number of scripts to make incremental changes to the database schema or data to support bug fixes and feature implementations.
packages holds the actual .gz files issued as part of the releases.
Packages are created from the contents of the dbscripts and updatescripts directories. First a holding directory needs to be created. Then the contents of the appropriate updatescripts directory and the changes to the dbscripts directory get copied. Next the package.xml file must be constructed. Finally, the holding directory gets bundled into a .gz file and tested. The Updater is platform-independent so these steps only need to be done once per release, not once per supported platform.
Make a directory to hold the package contents
The first step is to create a directory to hold the contents of the update package.
$ cd .../xtupleserver/trunk/packages $ mkdir OLDtoNEW
The OLD portion of the directory name describes the database version to which this upgrade package must be applied. The NEW portion of the directory name describes the database version which will be the result of applying this upgrade. Remember that the xTuple ERP client application must be run with a database of the same version. Otherwise you risk corrupting your data. The conventions for OLD and NEW are described in the Developers' Guide to Creating Releases of the PostBooks and OpenMFG Applications. For purposes of creating Updater package directories the resulting name should be all lower-case and without any spaces or punctuation; e.g. '2.3.2 Beta2' should be changed to '232beta2'.
Collect the update scripts and changed dbscripts
The second step is to update the appropriate updatescripts subdirectory with the changed dbscripts and create a few additional files necessary for upgrading. The hard part here is finding what changed but SVN can help with this.
The precise steps differ depending on how you've checked out the xtupleserver code. Try the following:
$ cd .../xtupleserver $ svn update $ ls
If the output of ls is the three directories branches, tags, and trunk then you can run the following command to find out what's changed and copy it to the proper updatescripts subdirectory:
$ cd .../xtupleserver/trunk/dbscripts
$ diff -x .svn -r -q ../../tags/ROLD/dbscripts . | \
awk '/^Files .* differ$/ {print $4; next }
/Only in \./ {sub(":", "/", $3) ; print $3 $NF; next }
{print}' | \
xargs file | \
grep -vw directory | \
cut -f1 -d: | \
xargs tar cf - | \
(cd ../updatescripts/from_OLD ; tar xvf - )Note that this is all two commands. One is a cd to put you in the right starting place. The second is a pipeline of commands that copies the changed files into place. The '\' at the end of the lines is an indication that the command carries over to another line of text. You have the option of typing the '\' at the end of the line and hitting RETURN or ENTER to continue with another line or not typing the '\' and just continuing typing. What does this long mess of a command do?
Compare the contents of the old tagged version of the dbscripts directory with the current directory. The cd command before the diff makes sure that the current directory is the version of dbscripts on the trunk.
The -x .svn option says to exclude the .svn subdirectory (we know this has changed and we don't care because it's just there for bookkeeping your checkout).
-r says to compare the files within any subdirectories that are found.
-q tells diff to summarize the output
The awk command converts the diff output into a list of files. The command has three parts:
extract the file name for any line of diff output that says the file changed
extract the file name for any line of diff output that says the file has been added
- print in its entirety any line that doesn't match either of the two preceding rules
The result of the awk command then gets passed to the file command, which identifies the type of the file.
The names of changed directories get stripped out by grep. We don't want the changed directories because we're going to bundle the changed files in a moment to copy them; we want only the changed files, not the entire directory contents.
The cut command then strips off the file type.
Then we call tar to bundle all of these files together to copy them en masse.
Finally we change to the updatescripts directory and unbundle the resulting files. The parentheses around the last part of this command are extremely important.
If the output of the ls command in the xtupleserver directory gives a different result then you'll have to do things a bit differently. Make sure that you have a checkout of the current trunk. Then also check out the tagged version of xtupleserver somewhere else. Then you can use the same pipeline described above with slight modifications for the changed directory paths.
With the changed dbscripts files copied to the updatescripts/from_OLDVER directory, you can now proceed to cleaning up the files that are here, adding a new one, and committing them all to the repository.
To clean up the existing update scripts, open each one in a text editor. Remove any BEGIN and COMMIT statements from these files - the Updater wraps the entire update in a single transaction and we don't want the individual pieces of the package to disrupt that. The result would be at best a spurious error message and at worst a corrupted database.
$ cd updatescripts/from_OLDVER $ vi * # or whatever your editor of choice is
Create a setVersion script containing the following SQL statement:
SELECT setMetric('OpenMFGServerVersion', 'NEWVER');where NEWVER is the destination version number of the database upgrade. This will cause the Updater to change the version number of the database to NEWVER at the end of the upgrade.
Now tell SVN that you want it to store all of the changed dbscript files and the new setVersion script in this updatescripts subdirectory:
$ svn add setVersion $ svn add api functions metasql triggers types views $ svn commit -m "copied over for release"
Copy the updatescripts subdirectory
The naming convention for the package directory differs from the naming for the updatescripts subdirectory, and the update package itself gets checked in somewhere else. To make these adjustments:
$ cd .../xtupleserver/trunk/packages $ mkdir OLDVERtoNEWVER $ cp -r .../xtupleserver/trunk/updatescripts/from_OLDVER/* OLDVERtoNEWVER
This creates a new directory for you to work in to adjust the contents of the Updater package.
Copy changed report definitions
Now add to that working directory the reports which have changed between the previous release and the one you're working on:
$ cd .../xtuple/trunk/share
$ diff -q .../xtuple/tags/ROLDVER/share/reports reports | \
awk '/^Files .* differ$/ {print $4; next }
/Only in reports/ {sub(":", "/", $3) ; print $3 $NF; next }
{print}' | \
xargs tar cf - | \
(cd .../xtupleserver/trunk/packages/OLDVERtoNEWVER ; tar xvf - )
Construct package.xml
Create an initUpgrade script. This is a precautionary measure that allows you to check if an upgrade was partially committed. This was a particular problem in version 1.0 of the Updater (an older version which has been superceded by newer versions) but can still happen if the update scripts have transaction handling code in them that wasn't removed earlier. This is a copy of the setVersion.sql script you created earlier with a minor change:
$ cp setVersion.sql initUpgrade $ edit initUpgrade
Change the version number in the second argument to the stored procedure so it has a '.' (period), followed by the old version, '-', and the new version. For example, change 3.1.0Beta2 to .3.1.0Beta-3.1.0Beta2.
You can now use the contents of this working directory to help create the package.xml:
$ find * -type f | \
sort -f | \
awk '/[Rr]eport.*xml/ { printf " <loadreport file=\"%s\" grade=\"0\" />\n", $0; next }
/package.xml/ { next }
/metasql.*mql/ { printf " <loadmetasql file=\"%s\" />\n", $0; next }
/setVersion.sql/ { printf " <finalscript file=\"%s\" />\n", $0; next }
{ printf " <script file=\"%s\" />\n", $0; next }' \
> package.xmlThis pipeline does the following:
- Searches for all of the files in the current directory.
- Sorts the list.
- Wraps each line in a best-guess at an appropriate XML element.
Writes the output to package.xml.
Now manually perform the following steps:
Find the line containing initUpgrade and move it to the top of the file.
Find the line containing setVersion.sql, move it to the end of the file.
Wrap the entire contents of the file in a <package> element. At the end of the file add a line containing just </package> and insert the following at the top of the file:
<package version="1.1" id="OLDVERtoNEWVER" developer="xTuple" updater="2.0.0BETA3">
<prerequisite type="Query" name="Checking PostBooks Server Version" >
<query>SELECT TRUE
FROM metric
WHERE metric_name = 'Application' AND metric_value = 'PostBooks';
</query>
<message>
This package requires that it be applied against the PostBooks Server Database.
</message>
</prerequisite>
<prerequisite type="Query" name="Checking PostBooks Server Version" >
<query>SELECT TRUE
FROM metric
WHERE (metric_name = 'OpenMFGServerVersion') AND
(metric_value IN ('NEWVER' OR '.OLDVER-NEWVER');
</query>
<message>
This package requires that it be applied against the OLDVER version of the PostBooks Server Database.
</message>
</prerequisite>Make sure you replace OLDVER and NEWVER with appropriate values.
You're almost done creating the file. Look for a line containing Precheck. If there is one then remove it and look at the contents of the Precheck file. Convert any queries you find there into <prerequisite> checks.
If you haven't already, save the file and exit the editor.
Create and test the package itself
Now that you have a collection of files to perform the upgrade, it's time to create the Updater .gz file and test it. There are two aspects to testing:
- Ensuring that the upgrade script runs without errors.
- Ensuring that the resulting database contains the desired structure.
First, however, you have to create the Updater package. Change to the parent directory, bundle the files, and save the work you've done so far:
$ cd .. $ tar czf pbOLDVERtoNEWVER.gz OLDVERtoNEWVER $ svn add pbOLDVERtoNEWVER.gz $ svn commit -m "untested update script pbOLDVERtoNEWVER.gz
Using a backup of a database from the OLDVER, create a new database to practice upgrading. For example, get a copy of the demo database backup from the previous version, create a database, and restore the demo backup to it.
Start the Updater and open the new package. If any of the prerequisite checks fail, fix the problems in the database and reopen the new package. Once all of the prerequisite checks pass, start the update.
Repeatedly fix problems with the package until you think all of the problems are resolved. You may need to change the order in which scripts are run or make changes to the scripts themselves. If you have to change individual scripts, don't forget to change them in updatescripts/from_OLDVER and dbscripts before copying them to the package directory and rebuilding the package.
Drop and reload the practice database and run the upgrade script all the way through again to make sure it runs cleanly.
Check in the revised package and any files you changed in updatescripts/from_OLDVER and dbscripts. This completes the first phase of making sure that the upgrade script runs without errors.
Now test that the upgraded database has the desired structure. The way xTuple has traditionally done this is by comparing the schema of the practice upgraded database with that of the dev database. This dev database is shared by the developers during the development cycle and is in essence the master reference database for the expected current schema.
Open the practice copy of the upgraded demo database with psql and run the \z command. This lists all of the tables and their access privileges. Look for lines with blank privileges. Fix the appropriate table creation scripts, copy them over, rebuild the package, and retest. In the dev database check for tables that are not owned by the admin user. If you find any, either delete them because they are not supposed to be part of the database or change the owner with ALTER TABLE.
Repeat this testing with the empty and quickstart databases.
Compare the sample databases with the latest version of dev. Generate HTML descriptions of the dev database and the upgraded demo, empty, and quickstart databases with postgresql_autodoc. There is an HTML template (currently stored in a proprietary CVS repository) which xTuple has developed to simplify the comparison process.
postgresql_autodoc --password -u admin -t html -d testdb postgresql_autodoc --password -u admin -t html -d dev
Remove any ^Ms from the generated output then compare the generated HTML. Resolve the differences, if there are any. Edit the scripts and rebuild the package as necessary. You may have to ask other developers for help understanding why some differences exist. Repeat until the differences have been minimized.
Check in any changes you made.
Test one final time by upgrading each of the three reference databases, generating the postgresql_autodoc for the reference databases and dev, and looking at the differences between the reference databases and dev.
Packages for Add-ons to xTuple ERP
The Updater has been used by xTuple for a long time to manage database upgrades. With the addition of application scripting and site-specific forms came the desire to bundle pieces of non-core functionality so they could be installed together to extend the xTuple ERP functionalty. Issue 6908 and the associated PackageManager specification expanded the role of the Updater to include the ability to load this non-core functionality.
The general process of packaging add-ons is similar to that of packing xTuple ERP upgrades but differs in some details and in the range of package syntax used. For example, the Updater used the name attribute of the root-level <package> element in package.xml to distinguish general updates from add-on packages. This name is used to create a separate schema in the xTuple ERP database, where all of the pieces of your package get stored (see below for details).
General Guidelines
Here are some general rules you should follow if you are creating an add-on package for xTuple ERP:
Name your package. Please give your package a name using the name attribute of the <package> element. This allows the updater to isolate your contributions from the core of xTuple ERP. You can add reports, scripts, user interface definitions, custom commands, etc. as part of your package and they will be found by xTuple ERP without any problems. Help yourself maintain your package, help the rest of us maintain xTuple ERP, and help the community of joint users of xTuple ERP and your package by keeping your contributions bundled together when deployed as well as for distribution.
If you create database objects, such as tables, views, and stored procedures, make sure that all of the run-time references to these objects qualify them with a schema prefix. If you create a new phones view in your package named phonedirectory and a script that queries this view, the script should write the query to SELECT ... FROM phonedirectory.phones .... This ensures that your phonedirectory package always refers to the phones view from your package and not the phones table or view that someone else might create in another package or that xTuple might eventually add to the core product.
- Keep your package as small as possible but still provide the functionality you set out to provide. You can always use package dependencies to manage simple relationships between packages instead of lumping a bunch of different features together simply because they share a common core. It would be better to have the core as its own package and each set of related features in their own packages, each marked as dependent on that core.
Always name yourself or your company as the developer of the add-on package using the developer attribute. You should also provide some way for people to contact you. You could use your company's web site URL for the developer attribute or put an email address and phone number in the package notes. This lets users find you to request support or features, report bugs, or pay you for your effort if that's your goal.
Use the <script> and <finalscript> tags sparingly if at all. These tags do not restrict the SQL statements that get performed and nothing that they do gets tied to your package except by the fact that the schema path has been set by the Updater before they get run. You could create tables or privileges using <script> tags but then you take the responsibility for registering them as part of your package.
Use the <createfunction>, <createtable>, <createtrigger>, and<createview> tags responsibly. The scripts named with these tags should do nothing more than the tag names imply - create functions with a particular name, create a single table, create a single trigger, and create a single view respectively - and the additional work to make them useful.
First Release
Creating a package for the first time is relatively simple because you have no backwards compatibility issues. This will certainly crop up if you need to upgrade your package contents in the future. For now, however, let's just talk about first-time package creation.
So, how do you start? Like with the xTuple ERP upgrades, you need to collect individual files, each of which contains one piece of data or performs one basic task. Here are the kinds of things you might want to include in your package:
- Screen definitions to add to xTuple ERP.
- Custom commands to provide access to your screens.
- Privileges to limit access to your screens.
- Scripts to run these screens or to supplement existing xTuple ERP windows.
- MetaSQL statements that can be shared by multiple windows.
- Report definitions.
- Images.
- New tables.
- New views.
- New stored procedures.
- New triggers.
- Additional rows in existing tables.
Create a directory to hold all of these pieces. It would be best to give this the same name you want your package to have. As you work on the components of your package, you should put them in this directory. xTuple recommends that you use a source code control system such as CVS or SVN to manage this directory and its components. If the package is relatively small then you can put all of the pieces directly into this directory. If the package grows too large it might be easier to create subdirectories for QtScript files, .ui files, report definitions, etc.
Once you have a directory to hold this stuff, create an essentially empty package.xml file. Create the <package> element with its id, name, developer, and version attributes and <pkgnotes> children to describe at a high level the package and its contents. The id attribute must match the directory name; otherwise the user will get an error when s/he tries to install your package. The version attribute can be whatever you like but it would be best to start at 0.1 or 1.0.
Use Qt Designer to generate the screen definitions. If you want to use xTuple's custom widgets you'll need to install the shared library that comes with xTuple ERP in the Designer plugins directory before you start designing your windows. Watch the DevelopmentEnvSetupGuide for changes to this but as of this writing (September, 2008, during the final phases of releasing xTuple ERP version 3.1.0) please use the Qt 4.3.2 version of Designer. Save the .ui files to your package directory and add <loadappui> elements to your package.xml file as you create new ones.
As you write QtScript to drive your windows, place them in individual files as well, one for each window that they will control. Add <loadappscript> elements to package.xml.
When you find that you need to add custom commands and privileges to your package, you can write these directly into the package.xml. See the syntax summary for details or the privilege and command examples above.
You can use OpenRPT utilities to build and test reports and MetaSQL definitions, then save them to your package directory. The openrpt binary has a report editor that can save reports either to a database or to a text file. The metasql application has a simple text editor you can use to create the statement as well as the ability to pass parameter values to your MetaSQL statement and execute the result against the database.
Image files can simply be copied to your directory. These can either be binary image files or uuencoded versions of those images.
Remember to explicitly add each file you create, including package.xml, to the source control system. Some, like RCS and CVS, will need to be told when you are adding binary files; others, like SVN, can figure this out on their own.
If you find that there is something you cannot add using these tags, then use the <script> and <finalscript> tags to write arbitrary SQL statements. What's the difference between <script> and <finalscript>? <script> lines are executed first, before any other tags (except <prerequisite>s of course), while <finalscript>s are executed last.
Subsequent Releases
Just like any other software product, you'll probably have to update your add-on package to add new functionality or make it run with updated versions of xTuple ERP. When you create update packages you might have to stretch some of the guidelines a bit. For example, you might want to create a package that upgrades existing old versions of your package but installs fresh if the database does not have a copy of your package already installed.
To illustrate some of the potential problems, let's talk about a package that contains a simple form, script, and table. In version 1.0 of your package the table had two columns and for version 1.1 you realized that you needed to add a column. The form and script don't cause any issues because the Updater just replaces the old 1.0 versions with the new 1.1 versions of these. They're just data stored in a table and the Updater knows to do an UPDATE rather than an INSERT for these.
The table change, however, will cause problems. For new installations of your package you want the table to be CREATEd while for existing installations you want it to be ALTERed. The package.xml line is the same in either case and, in fact, is exactly the same in both versions 1.0 and 1.1 of package.xml:
<createtable name="myTable" file="myTable.sql" />
In version 1.0 you might have written myTable.sql like so:
-- myTable.sql script for package version 1.0
CREATE TABLE myTable (
my_id SERIAL,
my_value INTEGER
);
REVOKE ALL ON TABLE myTable FROM PUBLIC;
GRANT ALL ON TABLE myTable TO admin;
GRANT ALL ON TABLE myTable TO GROUP openmfg;
REVOKE ALL ON TABLE myTable_myTable_id_seq FROM PUBLIC;
GRANT ALL ON TABLE myTable_myTable_id_seq TO admin;
GRANT ALL ON TABLE myTable_myTable_id_seq TO GROUP openmfg;This creates the table with two columns and makes sure that all xTuple ERP users but only xTuple ERP users can see the table and the sequence that controls the id column. The trick for the upgrade is to write myTable.sql so it can handle both the case where the table already exists and where the table does not. It might look something like this:
CREATE FUNCTION createMyTable() RETURNS INTEGER AS $$
DECLARE
_statement TEXT := '';
_version TEXT := '';
BEGIN
IF (EXISTS(SELECT relname
FROM pg_class, pg_namespace
WHERE relname='mytable'
AND relnamespace=pg_namespace.oid
AND nspname='mypackage')) THEN
_statement = 'ALTER TABLE myTable ADD my_comment TEXT;';
ELSE
_statement = 'CREATE TABLE myTable (' ||
' my_id SERIAL,' ||
' my_value INTEGER,' ||
' my_comment TEXT' ||
');';
END IF;
EXECUTE _statement;
RETURN 0;
END;
$$ LANGUAGE 'plpgsql';
SELECT createMyTable();
DROP FUNCTION createMyTable();
REVOKE ALL ON TABLE myTable FROM PUBLIC;
GRANT ALL ON TABLE myTable TO admin;
GRANT ALL ON TABLE myTable TO GROUP openmfg;
REVOKE ALL ON TABLE myTable_myTable_id_seq FROM PUBLIC;
GRANT ALL ON TABLE myTable_myTable_id_seq TO admin;
GRANT ALL ON TABLE myTable_myTable_id_seq TO GROUP openmfg;Note that this revised script does exactly one thing as its net effect: It ensures that the table exists with the desired format. If the table already exists, then it gets updated. If the table does not exist then it gets created. A stored procedure is created to do this work, gets executed, and is finally dropped.
Dependencies
If you followed the advice above and split your add-on into several packages, where one package depends on another to work properly, you'll have to make sure that the Updater recognizes this dependency. The package.xml supports a special <prerequisite> description for tracking dependencies. To pass the prerequisite check, the package described in the <dependson> element of the <prerequisite> must be installed:
- There must be a package with the required name.
If the <dependson> element has a version attribute then that exact version of that package must be installed.
If the developer attribute is given then the package must match the developer as well.
Here is a sample dependency prerequisite specification:
<prerequisite type="dependency" >
<dependson name="parentpkg" version="1.1" developer="myCompany"/>
<message>
parentpkg, written by myCompany, contains features my package needs.
Specific aspects of my package require version 1.1 of parentpkg.
</message>
</prerequisite>
Internal Details
Loading Sequence
When the Updater first opens a package the following things happen:
It looks for a package description in a file named package.xml or contents.xml. If it finds more than one, then it reports an error. If none is found, then it reports an error. If the package description is found in contents.xml then, a warning is written to the debugging output.
Then the package.xml is parsed. If the syntax of the file is incorrect; if the Updater version does not match the requirements of the package; or if there was a problem processing one of the element descriptions, messages are written to the Updater window. Fatal messages are written in red, warnings in orange. If any of the errors are fatal then the Updater stops processing the package description after all of these messages are written.
- If there were no fatal errors processing the package description, then the Updater checks the Prerequisites in the order listed in the package description. If one or more of the prerequisites failed then processing stops. If they all pass then the user is advised to back up the database and the Start Update button is enabled.
At this point the user has the option to process the update, open a different package, or quit. If s/he chooses to process the update, then s/he should click the Start Update button. When the Start Update button is pressed the Updater does the following in this order:
- A transaction is started.
If this package is not named with a name attribute, then it is treated as a database upgrade and the contents are written to the public database schema. If the package is named, then a new schema is created in the database. If there isn't already a schema with this package's name, a record is written to the pkghead table for it, and the schema path for the Updater is set to include the package schema.
- Elements are processed in a specific order:
<loadpriv>
<script>
<createfunction>
<createtable>
<createtrigger>
<createview>
<loadmetasql>
<loadreport>
<loadappui>
<loadappscript>
<loadcmd>
<loadimage>
<prerequisite> with dependson attributes - these are stored in the pkgdep tabl
<finalscript>
In the normal case any error immediately results in an error message and the update is immediately rolled back. If any errors were ignored during processing (see package.xml Reference) then the user is prompted to choose whether to accept the update or roll it back.
- If no errors occurred or the user chose to ignore them, the update is committed and the schema path is restored to its default. At this point the user can either load a new update package or quit.
Different types of element are loaded in the order listed above. Within a single type, elements are loaded in the order they are found in the package.xml file.
It is important to note the differences between named and unnamed packages. They are distinguished by whether the <package> element in the package description has a name attribute or not. If it has a name then the expectation is that this is an application add-on. If the package does not have a name then the expectation is that this is a database upgrade. Database upgrades are all processed with the default schema path, so changes are applied to the public schema unless the individual script explicitly changes this. Add-on packages are stored in their own schemas. As mentioned above the Updater creates a new schema named after the package and changes the run-time schema path. The Updater creates several tables in this new schema to hold schema-specific data:
pkgmetasql
pkgreport
pkguiform
pkgscript
pkgcmd
pkgcmdarg
pkgimage
These are all created as child tables of tables in the public schema, which makes for seamless integration from the application users' perspective. It also allows administrators to disable or enable packages - internally this is done by breaking or re-establishing the inheritance relationships.
package.xml Reference
Syntax of package.xml
Here is an approximate XML Schema representation of the package file format:
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:simpleType name="fullversionnumber">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\d+.\d+.\d+((wip|alpha|beta|rc)(\d)?)?" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="onerror">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(Default|Stop|Prompt|Ignore)" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="grade">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(highest|\d+|lowest)" />
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="enabled">
<xsd:restriction base="xsd:string">
<xsd:pattern value="(t(rue)?|f(alse)?|T(RUE)?|F(ALSE)?)" />
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="dbobj" mixed="true" >
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="name" use="required" />
<xsd:attribute name="file" use="required" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
<xsd:element minOccurs="0" /> <!-- text of comment -->
</xsd:sequence>
</xsd:complexType>
</xsd:complexType>
<xsd:element name="package">
<xsd:sequence>
<xsd:attribute name="id" use="required" />
<xsd:attribute name="name" use="optional" />
<xsd:attribute name="developer" use="optional" />
<xsd:attribute name="version" use="optional" type="xsd:decimal"/>
<xsd:attribute name="descrip" use="optional" />
<xsd:attribute name="updater" use="optional" type="fullversionnumber" />
<xsd:element name="pkgnotes" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="prerequisite">
<xsd:sequence>
<xsd:attribute name="name" use="optional" \>
<xsd:attribute name="type" type="(query|license|dependency)" />
<xsd:element name="query" />
<xsd:element name="dependson">
<xsd:sequence>
<xsd:attribute name="name" \>
<xsd:attribute name="version" type="decimal" \>
<xsd:attribute name="developer" \>
</xsd:sequence>
</xsd:element>
<xsd:element name="message" />
<xsd:element name="providedby" >
<xsd:sequence>
<xsd:attribute name="name" />
</xsd:sequence>
</xsd:element>
</xsd:sequence>
</xsd:element>
<xsd:element name="script" minOccurs="0" maxOccurs="unbounded">
<xsd:sequence>
<xsd:attribute name="file" use="optional" /> <!-- name and file are equiv, one is required -->
<xsd:attribute name="name" use="optional" /> <!-- file takes precedence -->
<xsd:attribute name="onerror" use="optional" type="onerror" />
</xsd:sequence>
</xsd:element>
<xsd:element name="createfunction" minOccurs="0" maxOccurs="unbounded" type="dbobj" />
<xsd:element name="createtable" minOccurs="0" maxOccurs="unbounded" type="dbobj" />
<xsd:element name="createtrigger" minOccurs="0" maxOccurs="unbounded" type="dbobj" />
<xsd:element name="createview" minOccurs="0" maxOccurs="unbounded" type="dbobj" />
<xsd:element name="loadmetasql" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="file" use="required" />
<xsd:attribute name="name" use="optional" />
<xsd:attribute name="group" use="optional" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="loadpriv" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="name" use="required" />
<xsd:attribute name="module" use="optional" /> <!-- defaults to Custom -->
<xsd:attribute name="onerror" use="optional" type="onerror" />
</xsd:sequence>
</xsd:element>
<xsd:element name="loadreport" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="file" use="required" />
<xsd:attribute name="name" use="optional" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
<xsd:attribute name="grade" use="optional" type="grade" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="loadappui" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="file" use="required" />
<xsd:attribute name="name" use="optional" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
<xsd:attribute name="order" use="optional" type="grade" />
<xsd:attribute name="enabled" use="optional" type="enabled" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="loadappscript" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="file" use="required" />
<xsd:attribute name="name" use="required" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
<xsd:attribute name="order" use="optional" type="grade" />
<xsd:attribute name="enabled" use="optional" type="enabled" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="loadcmd" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute or an <arg> child it's a comment -->
<xsd:sequence>
<xsd:attribute name="name" use="required" />
<xsd:attribute name="title" use="required" />
<xsd:attribute name="execitable" use="required" />
<xsd:attribute name="module" use="required" />
<xsd:attribute name="privname" use="optional" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
<xsd:element name="arg" minOccurs="0" maxOccurs="unbounded" >
<xsd:sequence>
<xsd:attribute name="value" use="required" />
<xsd:sequence>
</xsd:element>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="loadimage" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType mixed="true"> <!-- if it isn't an attribute it's a comment -->
<xsd:sequence>
<xsd:attribute name="file" use="required" />
<xsd:attribute name="name" use="required" />
<xsd:attribute name="onerror" use="optional" type="onerror" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:element name="finalscript" minOccurs="0" maxOccurs="unbounded">
<xsd:sequence>
<xsd:attribute name="file" use="required" /> <!-- name and file are equiv, one is required -->
<xsd:attribute name="name" use="optional" /> <!-- file takes precedence -->
<xsd:attribute name="onerror" use="optional" type="onerror" />
</xsd:sequence>
</xsd:element>
</xsd:sequence>
</xsd:element>
</xsd:schema>Notice the attribute onerror on most of the elements in the package description. This attribute lets you specify whether an error which occurs during processing should stop the update immediately, be ignored, be automatically retried, or if the Updater should ask the user which of these paths to take. The default is stop, which causes the upgrade to roll back. Be very careful if you decide to override this default for several reasons:
- You may confuse the user by asking a question s/he does not know how to answer.
You risk corrupting the data if you allow the Updater to ignore the failure, either by setting the behavior to ignore or by letting the user decide with prompt.
You risk locking the database for a long time, as the entire update is run in a single transaction, which is held open while waiting for the user to respond to prompts. If any failures were ignored, the Updater also puts up a dialog at the end of the update processing asking if s/he really wants to ignore these errors, which also holds the transaction open.
Special notes on report definitions
The name of the report in the <loadreport> element's name attribute is overridden by the content of the report file's <name> element. The description of the report in the <loadreport>'s comment text is overridden by the content of the report file's <description> element.
If a report's grade attribute is set to 'lowest', then the grade is set to the smallest unused grade for reports with this name. if the grade attribute is set to 'highest', then the grade is set to one greater than the highest used report_grade for reports with this name.
Special notes on MetaSQL definitions
The group, name, and comments assigned to a MetaSQL statement in the <loadmetasql> element are overridden by the contents of the loaded file. If the file has a comment on a line by itself that starts with the word group: (case insensitive) then this is the group value used for the MetaSQL statement. If the file has a name: comment then this is the name value used for the MetaSQL statement. If the file has a notes: comment then this is the comment value used for the MetaSQL statement. All comment lines following the notes: comment are included. For example:
-- Group:display
--naMe: fillList
SELECT * FROM cohead WHERE cohead_id = <? value("cohead_id") ?>;
-- NOTES: This
-- is a
--strange commentwill create an entry in the metasql table like this:
metasql_group |
display |
metqsql_name |
fillList |
metasql_notes |
This is a strange comment |
Special notes on.UI files
The order attribute of <loadappui> elements is handled much like grades are for reports. The difference is that uiforms may have multiple rows with the same order, so there is no special processing to avoid duplicates.
If the .ui file named in the file attribute does not have <ui> as the document's root element, then the load fails. The value given to the name attribute is overridden by the the name of the first <class> element in the .ui file.
Special notes on Application Scripts
The order attribute of <loadappscript> elements is handled the same was as for <loadappui> elements.
Special notes on Images
The Updater looks at the first few characters of image files before loading them into the database. If an individual file is a raw graphics file, then it is uuencoded before loading into the db. If the file is already uuencoded, then it gets loaded as is. The Updater supports the same image formats as the main xTuple ERP application does, which in turn is dependent on how Qt was built. See the DevEnvSetupGuide for Qt configuration options, including supported graphics formats.
Database Structure
The xTuple ERP application uses primarily the public schema in the application database. Add-on packages are written to their own schemas, named after the package itself. When the Updater scans package.xml it checks to see if this is a named add-on package or not. If it is an add-on, then it creates a schema to hold the package. If a schema with that name doesn't already exist, then it changes the search path to put this package-specific schema first. Any database objects that get created by the update, like tables and views, are then created in this package schema rather than in public.
The Updater uses a number of tables in the database. Most tables are only used by particular features of the Updater. As long as these tables exist, the Updater itself will work. If you don't use a certain feature then the Updater won't use that particular table. Here is a list of tables directly accessed by the Updater:
Table |
Used By |
Comments |
pkghead |
all parts |
only for add-on packages |
pkgitem |
all parts |
only for add-on packages |
cmd * |
<loadcmd> |
has sequence cmd_cmd_id_seq |
cmdarg * |
<loadcmd><arg> |
has sequence cmdarg_cmdarg_id_seq |
image * |
<loadimage> |
has sequence image_image_id_seq |
metasql * |
<loadmetasql> |
has sequence metasql_metasql_id_seq and accessed through a stored procedure savemetasql(TEXT, TEXT, TEXT, TEXT, BOOLEAN) RETURNS INTEGER |
priv * |
<loadpriv> |
has sequence priv_priv_id_seq |
report * |
<loadreport> |
has sequence report_report_id_seq |
script * |
<loadappscript> |
has sequence script_script_id_seq |
uiform * |
<loadappui> |
has sequence uiform_uiform_id_seq |
All of the tables marked with an asterisk (*) have the following in common:
The Updater creates a special child table called pkgparenttable in the package's schema for add-on packages. In version 2.0.0 of the Updater, all of these child tables are created as soon as the Updater creates the schema for the package. Hence parent tables with the proper names must be defined in the public schema.
The Updater creates two triggers on each of these package-specific child tables, one using the function _pkgparenttablebeforetrigger and the other using _pkgparenttablealtertrigger.
- The sequence used by the parent tables are shared with the child tables, which avoids having multiple records with the same id in the parent and child tables.
There are two reasons for using schemas to hold add-on packages:
- Schemas isolate the contents of one package from another, so changes to one package should not affect changes to another.
- By using child tables in the add-on package schema it becomes simple to disable packages - all that is required at the database level is detaching the child tables from their parents. This prevents the contents of the children from being shown when someone selects from the parent, so they effectively disappear. Re-enabling a package requires re-attaching the children to the parents.
