Up: PostgreSQL stuff | |
Prev: PostgreSQL Patches | Next: The cube puzzle |
Tagged Types are a pet project of mine that I came up with a while ago that just seemed like a really nice idea. The idea is that you take an ordinary datatype and add a tag to it and preserve that tag across various operators.
The obvious applications would be:
test=# select c1, print_currency(c1) from c; c1 | print_currency ------------+---------------- 232.44 USD | US$ 232.44 21.20 EUR | € 21.20 -13.44 AUD | AU$ -13.44 0.01 USD | US$ 0.01 14.00 AUD | AU$ 14.00 (5 rows) test=# select c1 * 5 from c where tag(c1) = 'USD'; ?column? ------------- 1162.20 USD 0.05 USD (2 rows) test=# select t,"timestamp"(t) from c; t | timestamp -----------------------------------------+--------------------- 2005-08-14 02:00:00+02 Europe/Amsterdam | 2005-08-14 02:00:00 2005-08-14 02:00:00+02 Australia/Sydney | 2005-08-14 10:00:00 2005-08-14 02:00:00+02 Asia/Hong_Kong | 2005-08-14 08:00:00 2005-08-14 02:00:00+02 America/New_York | 2005-08-13 20:00:00 2005-08-14 02:00:00+02 Asia/Kuwait | 2005-08-14 03:00:00 (5 rows)
As you can see there are some rough edges, especially with the timestamp code. That last example won't even work unless you have a recent version of PostgreSQL as it won't recognise the time zones. After all, this only defines the types, not the code to produce the answers. The tarball you can download below contains example SQL scripts for currencies, timestamps that remember timezones and locale specific text sorting.
SELECT create_tagged_type( 'currency', 'numeric(8,2)', 'currencies' );The first argument currency is the name of the type you want to create. The second argument is the base type. Note that you can specify the so called typmod of the type to specify things like precision. The third argument currencies is the table of tags to use.
What happens is that the module looks up the details for the base type and creates the new type with the correct parameters for storage. The storage is essentially what is used by the base type plus 4 bytes for the OID. For details see the FAQ.
When you insert a value it extracts the last word from the string (space delimited) as the tag. Hence you cannot have spaces in your tags. This tag is looked up in the tag table to check it is allowed. The remainder of the string is passed to the normal input function for the base type. These are then combined and stored as a unit.
# select create_tagged_operator( 'currency','*','numeric' ,'currency'); NOTICE: *(currency,numeric) => currency maps to *(numeric,numeric) => numeric # select create_tagged_operator( 'numeric', '*','currency','currency'); NOTICE: *(numeric,currency) => currency maps to *(numeric,numeric) => numeric # select create_tagged_operator( 'currency','/','currency','numeric' ); NOTICE: /(currency,currency) => numeric maps to /(numeric,numeric) => numeric # select create_tagged_operator( 'currency','/','numeric' ,'currency' ); NOTICE: /(currency,currency) => numeric maps to /(numeric,numeric) => numeric # select create_tagged_operator( 'currency','+','currency','currency'); NOTICE: +(currency,currency) => currency maps to +(numeric,numeric) => numeric # select create_tagged_operator( 'currency','-','currency','currency'); NOTICE: -(currency,currency) => currency maps to -(numeric,numeric) => numericAs you can see, it lets you create operators and automatically determines what base operator to use. This only produces simple operators. If the two arguments are both tagged, it requires them to have the same tag or it will error. If you want something more sophisticated, the auxiliary functions below will help.
Creating the operators above means you can now add two currency values or multiply a currency value by a plain number, but you can't add a currency type to a plain number or multiply two currency values. For simple examples like this this is enough.
CREATE TYPE currency_t ( value numeric(8,2), tag text );There are implicit casts defined between the auxiliary type and the tagged type so you can choose which to use in your functions. However, the auxiliary type allows you to access the value and tag directly, which may help.
This requires a little more explanation. Above I indicated that you needed to create a table with all valid tags. However, you are allowed to have more columns, providing meta-data for your functions. You may have been wondering how the print_currency in the example above knew how to format the value. Well, here is the definition of that function:
CREATE FUNCTION print_currency( currency ) RETURNS text AS 'SELECT to_char( value($1), (tagdata($1)).format )' LANGUAGE 'sql' STRICT STABLE;There is a second column in the currency table (called format) which describes the format to pass to to_char() for this value. This allows you to abstract away various details from your code.
Strictly speaking, if a value is printed with a tag that no longer there, it'll print the OID instead (and display a NOTICE). This should be enough for you to find them (ie WHERE tag(x) = '#94823') and either delete them or construct a new value with the correct tag.
I've given thought to having a kind of foreign key reference to give a error when you delete a tag in use, but that would require an index on every column using the type to be efficient. Also, a trigger on the tagtable to stop people updating entries may be useful. However, I decided that in the end it's the admin's responsibility. They should revoke all privileges from the tag table if they feel it's a risk.
CREATE FUNCTION abs(currency) RETURNS currency AS 'SELECT currency( abs( value($1) ), tag($1) )' LANGUAGE SQL STRICT IMMUTABLE;It's easy doing it by hand, you can just write it down. To write a function that determines the right types and base function and calling everything at the right time is anything but trivial.
Note, writing it as an SQL language function as above means it will be in-lined directly into your queries.
One side-effect of this is that if you do calculations with the currency type, the precision is enforced at each step. So if you divide by a number, the result will immediatly be rounded to 2 decimal places. If you have intermediate steps that require higher precision it may be worthwhile to extract the numeric value and operate on it directly, coercing it back to currency only at the end.
SELECT '2.5 ms-1'::physics_unit * '4 s'::physics_unit; returns: 10 mMaybe not exactly something you want in a database but it would be a great example of what user-defined types can do.
This is however just a hobby thing, I'm not using it myself for anything major (yet) but I did learn quite a bit about PostgreSQL internals while writing it.
Up: PostgreSQL stuff | |
Prev: PostgreSQL Patches | Next: The cube puzzle |