/*
 * Copyright (C) 2002 Stichting NLnet, Netherlands, stichting@nlnet.nl.
 * 
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND STICHTING NLNET
 * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
 * STICHTING NLNET BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
 * USE OR PERFORMANCE OF THIS SOFTWARE.
 * 
 * The development of Dynamically Loadable Zones (DLZ) for Bind 9 was
 * conceived and contributed by Rob Butler.
 * 
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the
 * above copyright notice and this permission notice appear in all
 * copies.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS" AND ROB BUTLER
 * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
 * ROB BUTLER BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
 * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
 * USE OR PERFORMANCE OF THIS SOFTWARE.
 */
 
DLZ PostgreSQL driver

Before reading this document, you should read the SDLZ API documentation.
While the SDLZ doc was written to help programmers write SDLZ drivers, it
also has an abundance of basic information that anyone using the DLZ
package should understand.  It will give you a good background to better
understand how this driver operates.

The Postgres driver is obviously meant to be used in combination with a
Postgres database.  This driver has been tested on Windows 2K and Redhat
Linux 7.2 using only Postgres 7.2.  Older versions of the library may work
but have not been tested.  The driver should build properly on any UN*X system
that Bind and Postgres both support.  Be sure to specify --with-dlz-postgres
when running configure so that the Postgres driver is built with Bind.  By
default DLZ and its drivers are not built.  When you specify a DLZ driver the
DLZ core is automatically built too.  The Postgres libpq libraries and header
files are required to build the driver and are not included in this package.
You can download the PostgreSQL source from www.postgresql.org.  The configure
script will search in the normal locations for the Postgres headers and
libraries.  You can specify the appropriate directory if it is not found by
appending a path to the Postgres configure option.
For example: --with-dlz-postgres=/path/to/files

To find out where your Postgres headers and libraries are you can run the
the pg_config command.  "pg_config --includedir" will return the path
to Postgres header files, and "pg_config --libdir" will return the path
Postgres library files.

The Postgres driver was built to be as flexible as possible.  Most "drivers"
tend to require a specific database schema for the driver to be able to
interact with the database.  This then requires modification to the driver to
support any variation of the schema, introducing new problems and limiting the
usefulness and adoption of the driver.  This driver has been built specifically
to avoid that problem.  Instead of imposing a database schema, the driver
accepts SQL queries with a few special tokens as parameters.  These queries are
then parsed and the tokens removed.  When a query is run, the token is replaced
with the appropriate value.  This allows a variety of database schemas to be
used without modification to the driver's code.  The only limitations are:

   1) The query must return the appropriate data types in the correct order.
   2) The query must use the correct pre-defined tokens in the query.
   3) The query must be written so that when the tokens are replaced with
      search values, the query is still valid.

Below is a sample of a proper dlz_postgres_driver configuration.  This
configuration segment would be contained in Bind's config (named.conf) file.
It is explained more below.  When you are developing your own Postgres driver
SQL queries, be sure to pass the following parameters to Bind "-g -d 1".  The
first "-g" tells bind to write all log messages to stdout instead of a log
file.  The second parameter "-d 1" sets Bind's debug level to 1.  The Postgres
driver will print each SQL query it is about to run when the debug level is set
to at least 1.  This can be very helpful in debugging SQL queries while you are
setting up the driver.  The SQL queries will be printed only when Bind is
trying to answer DNS queries, not when Bind loads.  Run a few sample DNS
queries to see the SQL queries that are used.

dlz "postgres zone" {
        database "postgres 2
        {host=localhost port=5432 dbname=dns_data user=rob}
        {select zone from dns_records where zone = '%zone%'}
        {select ttl, type, mx_priority, case when lower(type)='txt' then '\"' || data || '\"' else data end from dns_records where zone = '%zone%' and host = '%record%' and not (type = 'SOA' or type = 'NS')}
        {select ttl, type, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%' and (type = 'SOA' or type='NS')}
        {select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%'}
        {select zone from xfr_table where zone = '%zone%' and client = '%client%'}";
};   

The first line:
dlz "postgres zone" {

This line tells Bind we want to use a DLZ driver.  The word "dlz" is a new Bind
keyword added by the DLZ patch.  The next section "postgres zone" is the label
for this configuration segment.  It is used in any error messages Bind displays
while parsing its config file.  The last piece "{" starts the DLZ
configuration section in Bind's config file.

The second line:
database "postgres 2

This line is indented just to make it easier to read the configuration file.
The keyword "database" is the only parameter that can be specified in a DLZ
configuration segment.  It is required.  The double quote (") begins the
command line that is passed to the DLZ driver--in this case, the PostgreSQL
driver.  Notice that the command line is actually broken over many lines.  This
is done to make the configuration segment easier to read.  The next piece is
the word "postgres".  This is the official name of the DLZ postgres driver.  We
are telling Bind that we want to use the Postgres driver.  The word "postgres"
is located at argv[0].  I.E. This is the command line array passed to the
driver, and the driver name must always be at argv[0], it is not optional.
The last piece on the line is the number "2".  This parameter is located at
argv[1] and is required.  This tells Bind how many database connections we
want the Postgres driver to use.  When Bind is built single threaded, this
parameter is ignored BUT STILL REQUIRED!  The Postgres driver only uses one
connection when built for single threaded operation.  When built for
multithreaded operation, the driver will open the specified number of
connections and ALWAYS keep them open.  The driver does NOT release connections
during periods of inactivity and then re-open them when needed.  I considered
this approach and decided against it.  A DNS server should always be ready to
answer an onslaught of queries with all of its resources available.

The third line:
{host=localhost port=5432 dbname=dns_data user=rob}

This is the database connection string; it is required.  For more information
on PostgreSQL's database connection parameters, refer to the PostreSQL manual.
Notice that the contents of this line are surrounded by braces "{" and "}".
Braces can be used inside of DLZ command lines to group several items together
into one parameter.  The entire line above is one parameter located at argv[2].
Braces cannot be nested, so "{some commands {more commands} other stuff}" would
be invalid and have unpredictable results.  Spaces are not removed inside of
braces, so be careful.  For example, {some commands} would be "some commands", 
but { some commands } would be " some commands ".  Notice the extra spaces
in the second set!   Usually, for SQL queries this does not have a negative
effect, but it should still be avoided.  For this reason, the queries are all on
one line.  The use of braces is similar to the use of double quotes on a
standard command line.  Double quotes could not be used, as the entire DLZ
command line is already inside of double quotes and the escaping would be
nasty.  Additionally, single quotes were not used because they have special
meaning in SQL queries.  Using single quotes would have made escaping SQL
queries nasty, too.

Fourth line:
{select zone from dns_records where zone = '%zone%'}

This query is used by findzone() in the Postgres driver.  This parameter is
required and located at argv[3].  Notice the string %zone%.  This is one of
the tokens discussed previously.  When Bind needs to ask if a zone is supported
by the Postgres database, it will use the above query and replace %zone% with
the zone name it's asking about.  Also, notice that the %zone% is inside a set
of single quotes.  Bind makes no guesses about syntax.  Zone is always a
string, but DLZ doesn't know or care that SQL strings should be inside of
single quotes.  It's up to you to make sure the query will have the correct
syntax when %zone% is replaced.  The Postgres driver is smart enough, however,
to properly escape the zone string.  So "www.dom'ain.com" will be properly
escaped so as not to cause a problem when used in the query.  Only the token
%zone% should be used in the findzone() query.  Other tokens will be recognized
and parsed out, but they will never be replaced by any value because only zone
is used in the findzone() operation.  Results returned by the findzone query
are not really used so what you return and in what order doesn't really
matter.  What does matter is the number of rows (or tuples in PostgreSQL terms)
that are returned.  If zero rows are returned, the database does not support the
zone (i.e. it is not authoritative for the zone).  If at least 1 row is
returned, then the database does support the zone, and thus is authoritative
for it.  Findzone MUST return at least 1 row for a zone query before the
lookup query will ever be called.  In the Postgres driver, the find zone query
is also used as the first query during the allowzonexfr() query operation.
This lets allowzonexfr() determine if the zone is supported by the database,
before it attempts to determine if a zone transfer is allowed on the zone by
the client. 


Fifth line:
{select ttl, type, mx_priority, case when lower(type)='txt' then '\"' || data || '\"' else data end from dns_records where zone = '%zone%' and host = '%record%' and not (type = 'SOA' or type = 'NS')}

This query is used by lookup() in the Postgres driver.  This parameter is
required and located at argv[4].  Notice the string %record%.  This is the
second of the tokens discussed previously.  Once Bind has determined the zone
is supported by calling findzone(), it will call lookup() during domain name
resolution DNS queries.  Only the %zone% and %record% tokens are useful in a
lookup query.  You don't have to use both tokens if your database query doesn't
require it.  Again, you are responsible for making sure the syntax of the query
is correct once %zone% and %record% are replaced.  The driver will properly
escape the strings used to replace the tokens.  The token %zone% is replaced
with the zone name, and %record% is replaced with the host name we are
searching for.  If we are searching at the zone apex, then %record% is replaced
with the string "@" (i.e. a string consisting of the single at (@) character).
If we have searched for the host name and not found it lookup will search for a
"wild card" hostname.  When searching for a wild card hostname %record% is
replaced with "*".  If your lookup query will be returning TXT dns records be
sure to wrap your TXT data with double quotes before returning the data.

Let's say your TXT data is: a long example

If your SQL does not wrap the data in double quotes the DNS query will return

"a" "long" "example"

If you do wrap the TXT data with quotes the DNS query will return

"a long example"

Notice that there is no token to tell the query what type of DNS record we are
looking for.  The query should return all records matching the record and zone
parameters.  Returning NS and SOA records by lookup is optional.  If your
lookup query will not return NS and SOA records, you must implement an authority
query that will return those types of records.  If your lookup query will
return NS and SOA records, the authority query is not required.  You should NOT
implement an authority query if your lookup query will return NS and SOA
records, as it will cause an error!  In this sample query, we are artificially
preventing NS and SOA records from being returned by the query so that we can
show the use of the authority function later in this document.  The order and
type of data returned by a lookup query IS EXTREMELY IMPORTANT!!  Also, the
number of fields returned by the lookup query can very from 1 to an unlimited
number of fields.  How many fields are returned is also of great importance.
All of this is discussed later in the documentation, after we have introduced
all of the queries that can be passed to the Postgres driver.


Sixth line:
{select ttl, type, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%' and (type = 'SOA' or type='NS')}

This query is used by authority() in the Postgres driver.  This parameter is
optional and always located at argv[5].  Only the token %zone% is useful in
this query.  Other tokens will never be replaced with a value, although they
are parsed out if you use them.  Again, you are responsible for making sure the
syntax of the query is correct once %zone% is replaced.  The driver will
properly escape the string used to replace the token.  The token %zone% is
replaced with the zone name.  This query should be used to return NS and SOA
records if the lookup query does not.  If the lookup query will return NS and
SOA records, DO NOT USE an authority query, as it will cause errors!  Like the
lookup query, the number, type and order of fields returned by the query is
extremely important.  All of this is discussed later in the documentation.

Seventh line:
{select ttl, type, host, mx_priority, data, resp_person, serial, refresh, retry, expire, minimum from dns_records where zone = '%zone%'}

This query is used by allnodes() in the Postgres driver.  This parameter is
optional and always located at argv[6].  If your lookup function will return
SOA and NS data, and you will not be using an authority query, you still need
to specify an empty authority query so that the allnodes query will be located
at argv[6].  To specify an empty authority query, use "{}" as your authority
query.  Notice there are no characters or spaces between the brackets.  If you
accidentally use "{ }" the driver will think you are using a single space as
your authority query and error.  BE CAREFUL!  In order to support zone
transfer, you must specify an allnodes AND an allowzonexfr query.  If only one
query is supplied, zone transfers will fail!  This query should be used to
return ALL records in the zone. Notice in this query we have NOT taken the 
efforts to wrap TXT records with double quotes.  If you will have TXT records
in your database be sure to wrap the TXT records with double quotes in both the
lookup and allnode queries.  Only the token %zone% is useful in this query.
Other tokens will never be replaced with a value, although they are parsed out
if you use them.  Again, you are responsible for making sure the syntax of the
query is correct once %zone% is replaced.  The driver will properly escape the
string used to replace the token.  The token %zone% is replaced with the zone
name.  Like the lookup query, the number, type and order of fields returned by
the query is extremely important.  All of this is discussed later in the
documentation.


Eighth line:
{select zone from xfr_table where zone = '%zone%' and client = '%client%'}";

This query is used by allowzonexfr() in the Postgres driver.  This parameter is
optional and always located at argv[7].  In order to support zone transfer, you
must specify an allnodes AND an allowzonexfr query.  If only one query is
supplied, zone transfers will fail! Notice the string %client%.  This is the
third token discussed previously.  The %client% token is only useful in the
allowzonexfr query.  Like all the other queries, %zone% will be replaced with
the zone name.  The token %client% will be replaced with the client's IP
address.  The IP address will be either an IPV4 or IPV6 address, depending upon
how the client connected to the DNS server (using IPV4 or IPV6).  The client
address will be ASCII text like all the other token replacement strings.  Also,
both the %zone% and %client% replacement strings will be properly escaped.  You
are still responsible to make sure the query is correct once the tokens have
been replaced.  Like the findzone query, the results returned by the
allowzonexfr query are not really used, so what you return and in what order
doesn't really matter.  What does matter is the number of rows that are
returned.  If zero rows are returned, the client is not allowed to perform zone
transfers on this zone.  If at least 1 row is returned, the client is allowed
to perform zone transfers for this zone, and the allnodes query will be
executed next.  Before using this query, the Postgres driver will use the
findzone query to determine if the zone is supported by the database.  Only if
the zone is supported will the allowzonexfr query be run.  Also notice the 
double quote and semi-colon at the end of the line.  The double quote closes
the command line string that was started on the second line.  The semi-colon
is required after a bind parameter, and is part of Bind's standard config file
syntax.

  
Ninth line:
};

This closes the DLZ configuration section in Bind's config file.  It is part
of Bind's standard configuration file syntax.


Returned Fields:

Now that we have covered all the different types of queries that can be used
by the Postgres driver, we can discuss the number, type and order of fields
returned by the queries.  This can be a bit difficult to understand, so be sure
to read the next section of the documentation thoroughly and carefully!

The chart below shows the order in which fields should be returned in queries.
Field 1 is the left most field in a SQL query.  The chart below shows 12 fields
but not all fields are required in all queries.  You can divide your data up
into even more fields if you like, as long as the concatenation of the data
makes sense to Bind.  An explanation of how fields are used and concatenated
together is detailed below.

Order |    Name     |  Data type   |           Description
-------------------------------------------------------------------------------                
  1   | ttl         | string (num) | Time to live
  2   | type        | string       | DNS data type
  3   | host        | string       | Host name or IP address
  4   | mx_priority | string (num) | MX Priority
  5   | data        | string       | IP address / Host name / Full domain name
  6   | primary_ns  | string       | Primary name server for SOA record
  7   | resp_person | string       | Responsible person for SOA record
  8   | serial      | string (num) | Serial # for SOA record
  9   | refresh     | string (num) | Refresh time for SOA record
  10  | retry       | string (num) | Retry time for SOA record
  11  | expire      | string (num) | Expire time for SOA record
  12  | minimum     | string (num) | Min time for SOA record
  
Notice the data type column.  Some fields are labeled as "string (num)".
PostgreSQL only really returns strings from a database when using the libpq
functions.  The values of the fields may be held as integers, floats, whatever
by the database, but the return from the libpq functions is only a string.
Eventually, however, the fields marked with "string (num)" are turned into
numbers by Bind or the Postgres driver.  Your database can hold these fields
as strings or numbers, but they MUST be able to be properly parsed into a number
without any extra characters.  If they cannot, Bind will error and fail to
answer the DNS query.  I recommend the fields labeled as "string (num)" be held
as numbers in your database schema.

There are five queries that can be used by the Postgres driver.  Of those, only
the lookup, allnodes, and authority queries make use of the fields returned by
the query.  In order for the fields to be interpreted properly by the driver,
they must be in the correct order.

Allnodes:

The allnodes query is actually the simplest to understand because it makes use
of all the fields and has the fewest options.  When the allnodes() function
loops through the rows returned by the query, it passes that data back to Bind
using the dns_sdlz_putnamedrr() function.  This function takes five parameters
and has the following signature:

dns_sdlz_putnamedrr(allnodes, name, type, ttl, data)

The first parameter "allnodes" is used internally by the driver and Bind so you
can just ignore it.  The next parameter "name" is the hostname of the record or
field 3 from the chart above.  Next is "type"--this is the DNS data type of the
record, field 2 from the chart above.  Next is "ttl" or time to live, field 1
in our chart.  The last parameter is "data".  Data accepts all the other 
information about this record as a string.  This string must be in the proper
format for Bind to use it properly.

The data string is "built" by concatenating fields 4 through 12 together, with
a space added in between each.  Fortunately, the number of spaces in between
doesn't matter, as long as there is at least one.  This makes it easy for one
query to return a number of different DNS types.  If a field returned by the
allnodes query is NULL, nothing is appended by that field.  The driver then
appends a space to the end of the string and continues to the next field.
This is done until fields 4 through 12 (or however many fields you have in
the query) are processed.

For example, if we had an all nodes query that returned 12 fields, the row for
a mx record might have the following data:

ttl = 3600
type = MX
host = @
mx_priority = 20
data = mail
all other fields in the row are NULL.

This would result in the following call to dns_sdlz_putnamedrr().

dns_sdlz_putnamedrr(allnodes, "@", "MX", 3600, "20 mail       ")

Notice all the extra spaces at the end of the data string.  That is caused by
the other fields in the row being NULL.  This is correct operation, and Bind
will be able to understand and use this data.

In the same result set, say the next row is an NS record.

ttl = 3600
type = NS
host = @
data = NS1
all other fields in the row are NULL.

The call to dns_sdlz_putnamedrr() now would look like this:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1       ")

Notice the space at the beginning.  This is caused by the field mx_priority
being NULL.

Now obviously, only 4 fields are required for the dns_sdlz_putnamedrr()
function.  So if your query returns only four fields, it should work properly
as long as the first three are ttl, type, host (in that order), and the fourth
has all the "data" needed for the last parameter of dns_sdlz_putnamedrr().  The
advantage of splitting the "data" field into many fields in your database is
easier data management.

Relative return data:

Another important point is that the Postgres driver uses relative data.  In the
last example, the call to dns_sdlz_putnamedrr() looked like this:

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1       ")

In the "data" field, we only have NS1.  NS1 is not a fully qualified domain
name.  Bind only returns fully qualified answers though.  Our driver knows that
NS1 is relative to the zone we are searching in.  So if we were searching in
the zone "example.com" the above would actually result in Bind returning
"NS1.example.com" (case doesn't matter in DNS queries).  This makes it easy to
manage our DNS data in the database using separate fields for zone and host
names.  Sometimes, though, we want to return data that is NOT relative to the
zone.  For example, if the authoritative name server for this zone isn't
"ns1.example.com", but is "ns1.domain.com", we need to be able to properly
return that information to Bind so the DNS response will be correct.  To
override the default behavior, use an extra "." on the end of the domain name.

for example:

ttl = 3600
type = NS
host = @
data = NS1
all other fields in the row are NULL, our zone is "example.com"

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1       ")

tells bind our name server is "NS1.example.com"

-----

if data = NS1.domain.com

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com       ")

Tells Bind our name server is "NS1.domain.com.example.com".  This is correct
behavior!  It is not the right answer, but the behavior is correct.

-----

if data = NS1.domain.com.

dns_sdlz_putnamedrr(allnodes, "@", "NS", 3600, " NS1.domain.com.       ")

Tells Bind our name server is "NS1.domain.com".  This is the answer we want to
give.  Notice the extra "." at the end of the host name.  This tells Bind that
the domain name is absolute, and not to be used relative to our zone of
"example.com".

To summarize, the allnodes query MUST have at least 4 fields in the result set,
and those fields must be ttl, type, host and data.  The fields must be in that
order, and the data field (or fields concatenated together) must hold the
remainder of any data required for that DNS record type.  To return absolute
instead of relative hostname data, an extra "." should be used at the end of
the domain name.

Lookup and Authority:

Lookup and authority operate in a manner similar to allnodes.  Instead of
dns_sdlz_putnamedrr(), lookup and authority use dns_sdlz_putrr().  This
function is similar to dns_sdlz_putnamedrr(), except it only takes 4
parameters.  The function dns_sdlz_putrr() has the following signature:

dns_sdlz_putrr(lookup, type, ttl, data)

The first parameter "lookup" is used internally by the driver and Bind so you
can just ignore it.  The next three parameters "type", "ttl", and "data" are
all the same as in the dns_sdlz_putnamedrr() function.

Lookup and authority work a little differently in that they have default values.
If a result set for a lookup or authority query only has one field, the driver
will use the default value of "a" for type and 86400 for ttl.  The call to
dns_sdlz_putrr will look something like:

dns_sdlz_putrr(lookup, "a", 86400, "your_data_field_here")

The field returned by your result set should be just a hostname or IP address
(for reverse DNS queries).  This is fine for a lookup query, but the purpose
of the authority query is to return NS and SOA records, so an authority query
should never return just one field!

If the result set for a lookup or authority query only has two fields, the
driver will use the default value of 86400 for ttl.  The first field in the
result set MUST be the DNS record type.  The second field MUST be the DNS
data in the format expected by Bind.  The format is the same as discussed for
the allnodes query.

dns_sdlz_putrr(lookup, "dns_type_here", 86400, "data_field_here")

If the result set for a lookup or authority query has three fields, the driver
will not use any default values.  The first field in the result set MUST be the
ttl.  The second field MUST be the DNS data type, and the last field MUST be
the DNS data in the format expected by Bind.  The format is the same as
discussed for the allnodes query.

dns_sdlz_putrr(lookup, "dns_type_here", "ttl_field_here", "data_field_here")

If the result set for a lookup or authority query has more than three fields,
the driver will not use any default values.  The first field in the result set
MUST be the ttl.  The second field MUST be the DNS data type.  All remaining
fields will be concatenated together to create the last parameter passed to
dns_sdlz_putrr().  This string must be formatted as Bind expects it.  The format
is the same as discussed for the allnodes query.

dns_sdlz_putrr(lookup, "dns_type", "ttl_here", "concatenated_fields_here")

As with the allnodes query, append a "." to the end of a hostname if you need
to return an absolute hostname instead of a relative one.

It may be confusing at first to understand how to properly create an SQL query
for the Postgres driver.  But the flexibility afforded by this way of doing
things is very powerful.  It sure beats being limited to a set schema, or
building an entirely new driver!

