The error messages for an ODBC driver based off of the SimbaEngine SDK are all derived from exceptions. This article will go through how the error message system works, and how to use it when implementing your ODBC driver.
Understanding the Error Message System
There are a number of classes involved in the error message system:
- The many sub-classes of ErrorException
These classes work together to create a flexible error reporting system which can easily support multiple locales for your driver. To understand how they work together this article will walk through the steps of what happens when an exception is thrown in your DSII.
ExceptionBuilder and ExceptionUtilities are utility classes to make setting up and throwing exceptions easier. ExceptionBuilder can be used to automatically generate exceptions as described in the following section.
When an exception is thrown, it is typically thrown with a component ID and a message key. When the specific error message or native error code are needed from the exception, the IMessageSource and a locale are passed to the ErrorException::GetMessageText() or GetNativeErrorCode() functions. The message key, component ID, and locale are then passed to the IMessageSource and used to find the correct error message file, and the message key is then used to identify the correct error message to use. Additionally, there may be parameters passed to the IMessageSource to parameterize an error message. The format of the expected error message is detailed in the section below.
The default locale is set to “en-US” (English – American), however this can be changed by specifying DriverLocale in your driver registry section. To specify a per-connection locale, override GetLocale() for your DSIConnection subclass and return the desired locale. The locale is used to look up the error messages by using the base ErrorMessagesPath directory, and looking in the subdirectory with the corresponding locale. For instance, if the base directory was “c:messages”, the Italian (it-IT) locale would be located in the “c:messagesit-IT” directory. If a locale can’t be located, then the default will be used.
The locale is composed of a 2-letter (lower case) language code, and an optional 2-letter (upper case) country code. If a country code is included, the two codes must be separated by a hyphen (-). The language codes conform to the ISO 639-1 standard: http://www.loc.gov/standards/iso639-2/php/code_list.php. The country codes conform to the ISO 3166-1 Alpha-2 code standard: http://www.iso.org/iso/country_codes/iso-3166-1_decoding_table.htm
- en-US (English – United States)
- fr-CA (French – Canada)
- it-IT (Italian – Italy)
- de-DE (German – Germany)
- es-ES (Spanish – Spain (Traditional))
- ja (Japanese)
Using the Error Message System
To use the error system in your DSII, you must have registered your error messages file and component ID with the IMessageSource in your IDriver class. The Quickstart and Ultralight samples both have examples of how to do this.
Error messages are stored in XML files stored in a directory which is specified by the ErrorMessagesPath key. Locales are supported by directories with well-known locale names. For example, the default American English locale is stored in the “en-US” subdirectory, and the French version would be stored in the “fr-FR” subdirectory. Your IDriver subclass should register the base error messages file name with the IMessageSource class, along with your unique driver component ID. This associates the component ID with this message file, so that exceptions with that component ID will have their messages loaded from this file. The Quickstart and Ultralight sample drivers have examples of how this is done in their IDriver subclass constructors.
The sample drivers include useful macros for throwing exceptions in their driver header files, Quickstart.h and Ultralight.h. Copying and modifying these macros for your own driver will allow you to easily throw general exceptions, which have a SQL state of HY000. To create exceptions with other standard SQL states, or a custom SQL state, you must create the exception directly.
Typically when an exception is thrown, a message identifier or key is used which identifies the error message that should be looked up when the error message is read. This key is used so that the proper error message can be looked up from the correct locale, depending on what locale has been set. There may be situations where the error message is known when the exception is created, such as when the message originates from another system, and in this case you would set the error message and native error code directly instead of using a key.
The error message XML file has the following schema:
<!DOCTYPE Messages [
<!ELEMENT Messages (Package*)>
<!ELEMENT Package (Error*)>
<!ATTLIST Package ID CDATA #REQUIRED>
<!ATTLIST Package DefaultComponent CDATA #REQUIRED>
<!ELEMENT Error (#PCDATA)>
<!ATTLIST Error Key ID #REQUIRED>
<!ATTLIST Error Component CDATA #IMPLIED>
<!ATTLIST Error NativeErrorCode CDATA #REQUIRED>
<!ATTLIST Error Params CDATA "0">
A simple example of what a file may look like would be. Note that the schema has been omitted for brevity.
<?xml version="1.0" encoding="iso-8859-1"?>
<Package ID="101" DefaultComponent="SampleDriver">
<Error Key="InvalidAccess" NativeErrorCode="1">Invalid object access.</Error>
<Error Key="InvalidColumnIndex" Params="1" NativeErrorCode="2">Invalid column index: %1%.</Error>
<Error Key="RemoteAccessError" Params="2" NativeErrorCode="3">Error accessing remote system: %1% (%2%)</Error>
This shows 3 examples, one with no parameters, one with 1 parameter, and one with two parameters. Note the Key is what is used to identify the error message when throwning an exception, and the ID for the Package corresponds to the component ID that was registered with the IMessageSource. The NativeErrorCode is displayed to allow you to easily determine what message is shown, no matter the language that is displayed.
Using the above error messages file as an example, an error message would be shown as the following:
[SampleDriver][DSI] (1) Invalid object access.
The driver is the first item shown, followed by the component the exception originated from. The native error code is shown in brackets, and finally the actual error message is shown.
Occasionally you may have a situation where the user needs to be notified of something, but the notification should not stop the current action. An example might be that their login credentials are set to expire in 5 days, or that they’ve attempted to read a value which is being altered in some way. In cases like these, a warning would be more appropriate. Warnings let the user actions continue and allow the warning message to be returned to the user as well. When a warning is posted, the ODBC function will return SQL_SUCCESS_WITH_INFO rather than SQL_SUCCESS (unless an error is generated), and the application can call the relevant ODBC functions to fetch the warnings that were posted.
To post warnings from your DSII, you will use the IWarningListener class. There is an IWarningListener for the following core classes: IEnvironment, IConnection, and IStatement. The IWarningListener is registered once via the RegisterWarningListener() function when the class is created, and if you’re using the DSI* subclasses of the interfaces, you can access the IWarningListener through the GetWarningListener() functions. The IWarningListener from the IStatement class should be used for the IDataEngine and child classes, as well as the IQueryExecutor if not using the SQLEngine. An IWarningListener is also supplied in some function signatures, and the listeners that are supplied should be used in those functions rather than the IWarningListener from the IStatement.
Posting a warning is done via functions on the IWarningListener. There are multiple functions, which function in a way similar to the exception constructors when reporting an error. Warning messages are defined in the same place as the error messages, and can have parameters and locales in the same way. You can also create custom SQL states for your warnings via the IWarningListener by using the appropriate functions. The only difference between creating a warning and creating an error is that for errors you create and throw an exception and for warnings you call a function on an IWarningListener.