Introduction to PostgreSQL/PLJava, part 5: Operations and Indexes

By thebearinboulderNo Comments

This article discusses operators and indexes in PL/Java. Many user-defined types will not need the former but a hash index should be supported for their use in SQL join clauses.

Operators

Operators are normal PL/Java methods that are also marked as operators via the CREATE OPERATOR statement.

Basic arithmetic for rational numbers is supported as

  1.     public static Rational negate(Rational p) throws SQLException {
  2.         if (p == null) {
  3.             return null;
  4.         }
  5.         return new Rational(-p.getNumerator(), p.getDenominator());
  6.     }
  7.  
  8.     public static Rational add(Rational p, Rational q) throws SQLException {
  9.         if ((p == null) || (q == null)) {
  10.             return null;
  11.         }
  12.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
  13.                 BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
  14.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  15.         BigInteger gcd = n.gcd(d);
  16.         n = n.divide(gcd);
  17.         d = d.divide(gcd);
  18.         return new Rational(n.longValue(), d.longValue());
  19.     }
  20.  
  21.     public static Rational subtract(Rational p, Rational q) throws SQLException {
  22.         if ((p == null) || (q == null)) {
  23.             return null;
  24.         }
  25.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).subtract(
  26.                 BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
  27.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  28.         BigInteger gcd = n.gcd(d);
  29.         n = n.divide(gcd);
  30.         d = d.divide(gcd);
  31.         return new Rational(n.longValue(), d.longValue());
  32.     }
  33.  
  34.     public static Rational multiply(Rational p, Rational q) throws SQLException {
  35.         if ((p == null) || (q == null)) {
  36.             return null;
  37.         }
  38.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getNumerator()));
  39.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  40.         BigInteger gcd = n.gcd(d);
  41.         n = n.divide(gcd);
  42.         d = d.divide(gcd);
  43.         return new Rational(n.longValue(), d.longValue());
  44.     }
    public static Rational negate(Rational p) throws SQLException {
        if (p == null) {
            return null;
        }
        return new Rational(-p.getNumerator(), p.getDenominator());
    }

    public static Rational add(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
                BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

    public static Rational subtract(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).subtract(
                BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

    public static Rational multiply(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getNumerator()));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

and

  1.       CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rational
  2.           AS 'sandbox.Rational.negate'
  3.           LANGUAGE JAVA IMMUTABLE STRICT;
  4.  
  5.       CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)
  6.           RETURNS javatest.rational
  7.           AS 'sandbox.Rational.add'
  8.           LANGUAGE JAVA IMMUTABLE STRICT;
  9.  
  10.       CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)
  11.           RETURNS javatest.rational
  12.           AS 'sandbox.Rational.subtract'
  13.           LANGUAGE JAVA IMMUTABLE STRICT;
  14.  
  15.       CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)
  16.           RETURNS javatest.rational
  17.           AS 'sandbox.Rational.multiply'
  18.           LANGUAGE JAVA IMMUTABLE STRICT;
  19.  
  20.       CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)
  21.           RETURNS javatest.rational
  22.           AS 'sandbox.Rational.divide'
  23.           LANGUAGE JAVA IMMUTABLE STRICT;
  24.  
  25.       CREATE OPERATOR - (
  26.          rightarg = javatest.rational, procedure.rational_negate
  27.       );
  28.  
  29.       CREATE OPERATOR + (
  30.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_add,
  31.          commutator = +
  32.       );
  33.  
  34.       CREATE OPERATOR - (
  35.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_subtract
  36.       );
  37.  
  38.       CREATE OPERATOR * (
  39.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide,
  40.          commutator = *
  41.       );
  42.  
  43.       CREATE OPERATOR / (
  44.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide
  45.       );
      CREATE FUNCTION javatest.rational_negate(javatest.rational) RETURNS javatest.rational
          AS 'sandbox.Rational.negate'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_add(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.add'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_subtract(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.subtract'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_multiply(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.multiply'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_divide(javatest.rational, javatest.rational)
          RETURNS javatest.rational
          AS 'sandbox.Rational.divide'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE OPERATOR - (
         rightarg = javatest.rational, procedure.rational_negate
      );

      CREATE OPERATOR + (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_add,
         commutator = +
      );

      CREATE OPERATOR - (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_subtract
      );

      CREATE OPERATOR * (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide,
         commutator = *
      );

      CREATE OPERATOR / (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_divide
      );

The operator characters are one to 63 characters from the set “+ – * / < > = ~ ! @ # % ^ & | ` ?” with a few restrictions to avoid confusion with the start of SQL comments.

The commutator operator is a second operator (possibly the same) that has the same results if the left and right values are swapped. This is used by the optimizer.

The negator operator is one that the opposite results if the left and right values are swapped. It is only valid on procedures that return a boolean value. Again this is used by the optimizer.

Ordering Operators

Many UDTs can be ordered in some manner. This may be something obvious, e.g., ordering rational numbers, or something a bit more arbitrary, e.g., ordering complex numbers.

We can define ordering operations in the same manner as above. N.B., there is no longer anything special about these operators – with an unfamiliar UDT you can’t assume that < really means “less than”. The sole exception is “!=” which is always rewritten as “” by the parser.

  1.     public static int compare(Rational p, Rational q) {
  2.         if (p == null) {
  3.             return 1;
  4.         } else if (q == null) {
  5.             return -1;
  6.         }
  7.         BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
  8.         BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
  9.         return l.compareTo(r);
  10.     }
  11.  
  12.     public int compareTo(Rational p) {
  13.         return compare(this, p);
  14.     }
  15.  
  16.     public static int compare(Rational p, double q) {
  17.         if (p == null) {
  18.             return 1;
  19.         }
  20.         double d = p.doubleValue();
  21.         return (d < q) ? -1 : ((d == q) ? 0 : 1);
  22.     }
  23.  
  24.     public int compareTo(double q) {
  25.         return compare(this, q);
  26.     }
  27.  
  28.     public static boolean lessThan(Rational p, Rational q) {
  29.         return compare(p, q) < 0;
  30.     }
  31.  
  32.     public static boolean lessThanOrEquals(Rational p, Rational q) {
  33.         return compare(p, q) <= 0;
  34.     }
  35.  
  36.     public static boolean equals(Rational p, Rational q) {
  37.         return compare(p, q) = 0;
  38.     }
  39.  
  40.     public static boolean greaterThan(Rational p, Rational q) {
  41.         return compare(p, q) > 0;
  42.     }
  43.  
  44.     public static boolean lessThan(Rational p, double q) {
  45.         if (p == null) {
  46.             return false;
  47.         }
  48.         return p.compareTo(q) < 0;
  49.     }
  50.  
  51.     public static boolean lessThanOrEquals(Rational p, double q) {
  52.         if (p == null) {
  53.             return false;
  54.         }
  55.         return p.compareTo(q) = 0;
  56.     }
  57.  
  58.     public static boolean greaterThan(Rational p, double q) {
  59.         if (p == null) {
  60.             return true;
  61.         }
  62.         return p.compareTo(q) > 0;
  63.     }
    public static int compare(Rational p, Rational q) {
        if (p == null) {
            return 1;
        } else if (q == null) {
            return -1;
        }
        BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
        return l.compareTo(r);
    }

    public int compareTo(Rational p) {
        return compare(this, p);
    }

    public static int compare(Rational p, double q) {
        if (p == null) {
            return 1;
        }
        double d = p.doubleValue();
        return (d < q) ? -1 : ((d == q) ? 0 : 1);
    }

    public int compareTo(double q) {
        return compare(this, q);
    }

    public static boolean lessThan(Rational p, Rational q) {
        return compare(p, q) < 0;
    }

    public static boolean lessThanOrEquals(Rational p, Rational q) {
        return compare(p, q) <= 0;
    }

    public static boolean equals(Rational p, Rational q) {
        return compare(p, q) = 0;
    }

    public static boolean greaterThan(Rational p, Rational q) {
        return compare(p, q) > 0;
    }

    public static boolean lessThan(Rational p, double q) {
        if (p == null) {
            return false;
        }
        return p.compareTo(q) < 0;
    }

    public static boolean lessThanOrEquals(Rational p, double q) {
        if (p == null) {
            return false;
        }
        return p.compareTo(q) = 0;
    }

    public static boolean greaterThan(Rational p, double q) {
        if (p == null) {
            return true;
        }
        return p.compareTo(q) > 0;
    }

Note that I’ve defined methods to compare either two rational numbers or one rational number and one double number.

  1.       CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)
  2.           RETURNS bool
  3.           AS 'sandbox.Rational.lessThan'
  4.           LANGUAGE JAVA IMMUTABLE STRICT;
  5.  
  6.       CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)
  7.           RETURNS bool
  8.           AS 'sandbox.Rational.lessThanOrEquals'
  9.           LANGUAGE JAVA IMMUTABLE STRICT;
  10.  
  11.       CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)
  12.           RETURNS bool
  13.           AS 'sandbox.Rational.equals'
  14.           LANGUAGE JAVA IMMUTABLE STRICT;
  15.  
  16.       CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)
  17.           RETURNS bool
  18.           AS 'sandbox.Rational.greaterThanOrEquals'
  19.           LANGUAGE JAVA IMMUTABLE STRICT;
  20.  
  21.       CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)
  22.           RETURNS bool
  23.           AS 'sandbox.Rational.greaterThan'
  24.           LANGUAGE JAVA IMMUTABLE STRICT;
  25.  
  26.       CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)
  27.           RETURNS int
  28.           AS 'sandbox.Rational.compare'
  29.           LANGUAGE JAVA IMMUTABLE STRICT;
  30.  
  31.       CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)
  32.           RETURNS bool
  33.           AS 'sandbox.Rational.lessThan'
  34.           LANGUAGE JAVA IMMUTABLE STRICT;
  35.  
  36.       CREATE FUNCTION javatest.rational_le(javatest.rational, float8)
  37.           RETURNS bool
  38.           AS 'sandbox.Rational.lessThanOrEquals'
  39.           LANGUAGE JAVA IMMUTABLE STRICT;
  40.  
  41.       CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)
  42.           RETURNS bool
  43.           AS 'sandbox.Rational.equals'
  44.           LANGUAGE JAVA IMMUTABLE STRICT;
  45.  
  46.       CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)
  47.           RETURNS bool
  48.           AS 'sandbox.Rational.greaterThanOrEquals'
  49.           LANGUAGE JAVA IMMUTABLE STRICT;
  50.  
  51.       CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)
  52.           RETURNS bool
  53.           AS 'sandbox.Rational.greaterThan'
  54.           LANGUAGE JAVA IMMUTABLE STRICT;
  55.  
  56.       CREATE OPERATOR < (
  57.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
  58.          commutator = > , negator = >= ,
  59.          restrict = scalarltsel, join = scalarltjoinsel, merges
  60.       );
  61.  
  62.       CREATE OPERATOR <= (
  63.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
  64.          commutator = >= , negator = > ,
  65.          restrict = scalarltsel, join = scalarltjoinsel, merges
  66.       );
  67.  
  68.       CREATE OPERATOR = (
  69.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_eq,
  70.          commutator = = , negator = <>, hashes, merges
  71.       );
  72.  
  73.       CREATE OPERATOR >= (
  74.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
  75.          commutator = <= , negator = < ,
  76.          restrict = scalarltsel, join = scalarltjoinsel, merges
  77.       );
  78.  
  79.       CREATE OPERATOR > (
  80.          leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
  81.          commutator = <= , negator = < ,
  82.          restrict = scalargtsel, join = scalargtjoinsel, merges
  83.       );
  84.  
  85.       CREATE OPERATOR < (
  86.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_lt,
  87.          commutator = > , negator = >=
  88.       );
  89.  
  90.       CREATE OPERATOR <= (
  91.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_le,
  92.          commutator = >= , negator = >
  93.       );
  94.  
  95.       CREATE OPERATOR = (
  96.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_eq,
  97.          commutator = = , negator = <>
  98.       );
  99.  
  100.       CREATE OPERATOR >= (
  101.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_ge,
  102.          commutator = <= , negator = <
  103.       );
  104.  
  105.       CREATE OPERATOR > (
  106.          leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_gt,
  107.          commutator = < , negator = <=
  108.       );
      CREATE FUNCTION javatest.rational_lt(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.lessThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_le(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.lessThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_eq(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.equals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_ge(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.greaterThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_gt(javatest.rational, javatest.rational)
          RETURNS bool
          AS 'sandbox.Rational.greaterThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_cmp(javatest.rational, javatest.rational)
          RETURNS int
          AS 'sandbox.Rational.compare'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_lt(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.lessThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_le(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.lessThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_eq(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.equals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_ge(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.greaterThanOrEquals'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE FUNCTION javatest.rational_gt(javatest.rational, float8)
          RETURNS bool
          AS 'sandbox.Rational.greaterThan'
          LANGUAGE JAVA IMMUTABLE STRICT;

      CREATE OPERATOR < (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
         commutator = > , negator = >= ,
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR <= (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
         commutator = >= , negator = > ,
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR = (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_eq,
         commutator = = , negator = <>, hashes, merges
      );

      CREATE OPERATOR >= (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_lt,
         commutator = <= , negator = < ,
         restrict = scalarltsel, join = scalarltjoinsel, merges
      );

      CREATE OPERATOR > (
         leftarg = javatest.rational, rightarg = javatest.rational, procedure = javatest.rational_le,
         commutator = <= , negator = < ,
         restrict = scalargtsel, join = scalargtjoinsel, merges
      );

      CREATE OPERATOR < (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_lt,
         commutator = > , negator = >=
      );

      CREATE OPERATOR <= (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_le,
         commutator = >= , negator = >
      );

      CREATE OPERATOR = (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_eq,
         commutator = = , negator = <>
      );

      CREATE OPERATOR >= (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_ge,
         commutator = <= , negator = <
      );

      CREATE OPERATOR > (
         leftarg = javatest.rational, rightarg = float8, procedure = javatest.rational_gt,
         commutator = < , negator = <=
      );

Restrict is an optimization estimator procedure. It’s usually safe to use the appropriate standard procedure.

Join is an optimization estimator procedure. It’s usually safe to use the appropriate standard procedure.

Hashes indicates that the operator can be used in hash joins.

Merges indicates that the operator can be used in merge joins.

Indexes

Indexes are used in three places – to enforce uniqueness constraints and to speed up WHERE and JOIN clauses.

  1.     -- btree join
  2.   CREATE OPERATOR CLASS rational_ops
  3.       DEFAULT FOR TYPE javatest.rational USING btree AS
  4.         OPERATOR        1       < ,
  5.         OPERATOR        2       <= ,
  6.         OPERATOR        3       = ,
  7.         OPERATOR        4       >= ,
  8.         OPERATOR        5       > ,
  9.         FUNCTION        1       javatest.rational_cmp(javatest.rational, javatest.rational);
  10.  
  11.     -- hash join
  12.    CREATE OPERATOR CLASS rational_ops
  13.       DEFAULT FOR TYPE javatest.rational USING hash AS
  14.         OPERATOR        1       = ,
  15.         FUNCTION        1       javatest.rational_hashCode(javatest.rational);
    -- btree join
  CREATE OPERATOR CLASS rational_ops
      DEFAULT FOR TYPE javatest.rational USING btree AS
        OPERATOR        1       < ,
        OPERATOR        2       <= ,
        OPERATOR        3       = ,
        OPERATOR        4       >= ,
        OPERATOR        5       > ,
        FUNCTION        1       javatest.rational_cmp(javatest.rational, javatest.rational);

    -- hash join
   CREATE OPERATOR CLASS rational_ops
      DEFAULT FOR TYPE javatest.rational USING hash AS
        OPERATOR        1       = ,
        FUNCTION        1       javatest.rational_hashCode(javatest.rational);

Operator Families

Finally, PostgreSQL has the concept of “Operator Families” that group related operator classes under a single umbrella. For instance you might have one family that supports cross-comparison between int2, int4 and int8 values. Each can be specified individually but by creating an operator family you give a few more hints to the PostgreSQL optimizer.

More Information

CREATE OPERATOR (PostgreSQL)

CREATE OPERATOR CLASS (PostgreSQL)

CREATE OPERATOR FAMILY (PostgreSQL)

Operator Optimization (PostgreSQL)

Interfacing Extensions To Indexes (PostreSQL)

java

Introduction to PostgreSQL PL/Java, part 4: User Defined Types

By thebearinboulder1 Comment

User-defined types in the database are controversial. They’re not standard – at some point the DBA has to create them – and this introduces portability issues. Standard tools won’t know about them. You must access them via the ‘struct’ methods in ResultSets and PreparedStatements.

On the other hand there are a LOT of things that are otherwise only supported as byte[]. This prevents database functions and stored procedures from easily manipulating them.

What would be a good user-defined type? It must be atomic and it must be possible to do meaningful work via stored procedures. N.B., a database user-defined type is not the same thing as a java class. Nearly all java classes should be stored as standard tuples and you should only use database UDTs if there’s a compelling reason.

A touchstone I like is asking whether you’re ever tempted to cache immutable information about the type, vs. about the tuple, in addition to the object itself. E.g., a X.509 digital certificate has a number of immutable fields that would be valid search terms but it’s expensive to extract that information for every row. (Sidenote: you can use triggers to extract the information when the record is inserted and updated. This ensures the cached values are always accurate.)

Examples:

  • complex numbers (stored procedures: arithmetic)
  • rational numbers (stored procedures: arithmetic)
  • galois field numbers (stored procedures: arithmetic modulo a fixed value)
  • images (stored procedures: get dimensions)
  • PDF documents (stored procedures: extract elements)
  • digital certificates and private keys (stored procedures: crypto)

Something that should also be addressed is the proper language for implementation. It’s easy to prototype in PL/Java but you can make a strong argument that types should be ultimately implemented as a standard PostgreSQL extensions since they’re more likely to be available in the future when you’re looking at a 20-year-old dump. In some important ways this is just a small part of the problem – the issue isn’t whether the actual storage and function implementation is written in C or java, it’s how it’s tied into the rest of the system.

PL/Java Implementation

A PL/Java user defined type must implement the java.sql.SQLData interface, a static method that creates the object from a String, and an instance method that creates a String from the object. These methods must complementary – it must be possible to run a value through a full cycle in either direction and get the original value back.

N.B., this is often impossible with doubles – this is why you get numbers like 4.000000001 or 2.999999999. In these cases you have do to the best you can and warn the user.

In many cases an object can be stored more efficiently in a binary format. In PostgreSQL terms these are TOAST types. This is handled by implementing two new methods that work with SQLInput and SQLOutput streams.

A simple implementation of a rational type follows.

  1. public class Rational implements SQLData {
  2.     private long numerator;
  3.     private long denominator;
  4.     private String typeName;
  5.  
  6.     public static Rational parse(String input, String typeName)
  7.             throws SQLException {
  8.         Pattern pattern = Pattern.compile("(-?[0-9]+)( */ *(-?[0-9]+))?");
  9.         Matcher matcher = pattern.matcher(input);
  10.         if (!matcher.matches()) {
  11.             throw new SQLException("Unable to parse rational from string \"" + input
  12.                     + '"');
  13.         }
  14.         if (matcher.groupCount() == 3) {
  15.             if (matcher.group(3) == null) {
  16.                 return new Rational(Long.parseLong(matcher.group(1)));
  17.             }
  18.             return new Rational(Long.parseLong(matcher.group(1)),
  19.                     Long.parseLong(matcher.group(3)));
  20.         }
  21.         throw new SQLException("invalid format: \"" + input
  22.                 + '"');
  23.     }
  24.  
  25.     public Rational(long numerator) throws SQLException {
  26.         this(numerator, 1);
  27.     }
  28.  
  29.     public Rational(long numerator, long denominator) throws SQLException {
  30.         if (denominator == 0) {
  31.             throw new SQLException("demominator must be non-zero");
  32.         }
  33.  
  34.         // do a little bit of normalization
  35.         if (denominator < 0) {
  36.             numerator = -numerator;
  37.             denominator = -denominator;
  38.         }
  39.  
  40.         this.numerator = numerator;
  41.         this.denominator = denominator;
  42.     }
  43.  
  44.     public Rational(int numerator, int denominator, String typeName)
  45.             throws SQLException {
  46.         this(numerator, denominator);
  47.         this.typeName = typeName;
  48.     }
  49.  
  50.     public String getSQLTypeName() {
  51.         return typeName;
  52.     }
  53.  
  54.     public void readSQL(SQLInput stream, String typeName) throws SQLException {
  55.         this.numerator = stream.readLong();
  56.         this.denominator = stream.readLong();
  57.         this.typeName = typeName;
  58.     }
  59.  
  60.     public void writeSQL(SQLOutput stream) throws SQLException {
  61.         stream.writeLong(numerator);
  62.         stream.writeLong(denominator);
  63.     }
  64.  
  65.     public String toString() {
  66.         String value = null;
  67.         if (denominator == 1) {
  68.             value = String.valueOf(numerator);
  69.         } else {
  70.             value = String.format("%d/%d", numerator, denominator);
  71.         }
  72.         return value;
  73.     }
  74.  
  75.     /*
  76.      * Meaningful code that actually does something with this type was
  77.      * intentionally left out.
  78.      */
  79. }
public class Rational implements SQLData {
    private long numerator;
    private long denominator;
    private String typeName;

    public static Rational parse(String input, String typeName)
            throws SQLException {
        Pattern pattern = Pattern.compile("(-?[0-9]+)( */ *(-?[0-9]+))?");
        Matcher matcher = pattern.matcher(input);
        if (!matcher.matches()) {
            throw new SQLException("Unable to parse rational from string \"" + input
                    + '"');
        }
        if (matcher.groupCount() == 3) {
            if (matcher.group(3) == null) {
                return new Rational(Long.parseLong(matcher.group(1)));
            }
            return new Rational(Long.parseLong(matcher.group(1)),
                    Long.parseLong(matcher.group(3)));
        }
        throw new SQLException("invalid format: \"" + input
                + '"');
    }

    public Rational(long numerator) throws SQLException {
        this(numerator, 1);
    }

    public Rational(long numerator, long denominator) throws SQLException {
        if (denominator == 0) {
            throw new SQLException("demominator must be non-zero");
        }

        // do a little bit of normalization
        if (denominator < 0) {
            numerator = -numerator;
            denominator = -denominator;
        }

        this.numerator = numerator;
        this.denominator = denominator;
    }

    public Rational(int numerator, int denominator, String typeName)
            throws SQLException {
        this(numerator, denominator);
        this.typeName = typeName;
    }

    public String getSQLTypeName() {
        return typeName;
    }

    public void readSQL(SQLInput stream, String typeName) throws SQLException {
        this.numerator = stream.readLong();
        this.denominator = stream.readLong();
        this.typeName = typeName;
    }

    public void writeSQL(SQLOutput stream) throws SQLException {
        stream.writeLong(numerator);
        stream.writeLong(denominator);
    }

    public String toString() {
        String value = null;
        if (denominator == 1) {
            value = String.valueOf(numerator);
        } else {
            value = String.format("%d/%d", numerator, denominator);
        }
        return value;
    }

    /*
     * Meaningful code that actually does something with this type was
     * intentionally left out.
     */
}

and

  1.     /* The shell type */
  2.     CREATE TYPE javatest.rational;
  3.  
  4.     /* The scalar input function */
  5.     CREATE FUNCTION javatest.rational_in(cstring)
  6.       RETURNS javatest.rational
  7.       AS 'UDT[sandbox.Rational] input'
  8.       LANGUAGE java IMMUTABLE STRICT;
  9.  
  10.     /* The scalar output function */
  11.     CREATE FUNCTION javatest.rational_out(javatest.rational)
  12.       RETURNS cstring
  13.       AS 'UDT[sandbox.Rational] output'
  14.       LANGUAGE java IMMUTABLE STRICT;
  15.  
  16.     /* The scalar receive function */
  17.     CREATE FUNCTION javatest.rational_recv(internal)
  18.       RETURNS javatest.rational
  19.       AS 'UDT[sandbox.Rational] receive'
  20.       LANGUAGE java IMMUTABLE STRICT;
  21.  
  22.     /* The scalar send function */
  23.     CREATE FUNCTION javatest.rational_send(javatest.rational)
  24.       RETURNS bytea
  25.       AS 'UDT[sandbox.Rational] send'
  26.       LANGUAGE java IMMUTABLE STRICT;
  27.  
  28.     CREATE TYPE javatest.rational (
  29.       internallength = 16,
  30.       input = javatest.rational_in,
  31.       output = javatest.rational_out,
  32.       receive = javatest.rational_recv,
  33.       send = javatest.rational_send,
  34.       alignment = int);
    /* The shell type */
    CREATE TYPE javatest.rational;

    /* The scalar input function */
    CREATE FUNCTION javatest.rational_in(cstring)
      RETURNS javatest.rational
      AS 'UDT[sandbox.Rational] input'
      LANGUAGE java IMMUTABLE STRICT;

    /* The scalar output function */
    CREATE FUNCTION javatest.rational_out(javatest.rational)
      RETURNS cstring
      AS 'UDT[sandbox.Rational] output'
      LANGUAGE java IMMUTABLE STRICT;

    /* The scalar receive function */
    CREATE FUNCTION javatest.rational_recv(internal)
      RETURNS javatest.rational
      AS 'UDT[sandbox.Rational] receive'
      LANGUAGE java IMMUTABLE STRICT;

    /* The scalar send function */
    CREATE FUNCTION javatest.rational_send(javatest.rational)
      RETURNS bytea
      AS 'UDT[sandbox.Rational] send'
      LANGUAGE java IMMUTABLE STRICT;

    CREATE TYPE javatest.rational (
      internallength = 16,
      input = javatest.rational_in,
      output = javatest.rational_out,
      receive = javatest.rational_recv,
      send = javatest.rational_send,
      alignment = int);

Type modifiers

PostgreSQL allows types to have modifiers. Examples are in ‘varchar(200)’ or ‘numeric(8,2)’.

PL/Java does not currently support this functionality (via the ‘typmod_in’ and ‘typmod_out’ methods) but I have submitted a request for it.

Casts

Custom types aren’t particularly useful if all you can do is store and retrieve the values as opaque objects. Why not use bytea and be done with it?

In fact there are many UDTs where it makes sense to be able to cast a UDT to a different type. Numeric types, like complex or rational numbers, should be able to be converted to and from the standard integer and floating number types (albeit with limitations).

This should be done with restraint.

Casts are implemented as single argument static methods. In the java world these methods are often named newInstance so I’m doing the same here.

  1.     public static Rational newInstance(String input) throws SQLException {
  2.         if (input == null) {
  3.             return null;
  4.         }
  5.         return parse(input, "javatest.rational");
  6.     }
  7.  
  8.     public static Rational newInstance(int value) throws SQLException {
  9.         return new Rational(value);
  10.     }
  11.  
  12.     public static Rational newInstance(Integer value) throws SQLException {
  13.         if (value == null) {
  14.             return null;
  15.         }
  16.         return new Rational(value.intValue());
  17.     }
  18.  
  19.     public static Rational newInstance(long value) throws SQLException {
  20.         return new Rational(value);
  21.     }
  22.  
  23.     public static Rational newInstance(Long value) throws SQLException {
  24.         if (value == null) {
  25.             return null;
  26.         }
  27.         return new Rational(value.longValue());
  28.     }
  29.  
  30.     public static Double value(Rational value) throws SQLException {
  31.         if (value == null) {
  32.             return null;
  33.         }
  34.         return value.doubleValue();
  35.     }
    public static Rational newInstance(String input) throws SQLException {
        if (input == null) {
            return null;
        }
        return parse(input, "javatest.rational");
    }

    public static Rational newInstance(int value) throws SQLException {
        return new Rational(value);
    }

    public static Rational newInstance(Integer value) throws SQLException {
        if (value == null) {
            return null;
        }
        return new Rational(value.intValue());
    }

    public static Rational newInstance(long value) throws SQLException {
        return new Rational(value);
    }

    public static Rational newInstance(Long value) throws SQLException {
        if (value == null) {
            return null;
        }
        return new Rational(value.longValue());
    }

    public static Double value(Rational value) throws SQLException {
        if (value == null) {
            return null;
        }
        return value.doubleValue();
    }

and

  1.   CREATE FUNCTION javatest.rational_string_as_rational(varchar) RETURNS javatest.rational
  2.         AS 'sandbox.Rational.newInstance'
  3.         LANGUAGE JAVA IMMUTABLE STRICT;
  4.  
  5.   CREATE FUNCTION javatest.rational_int_as_rational(int4) RETURNS javatest.rational
  6.         AS 'sandbox.Rational.newInstance'
  7.         LANGUAGE JAVA IMMUTABLE STRICT;
  8.  
  9.   CREATE FUNCTION javatest.rational_long_as_rational(int8) RETURNS javatest.rational
  10.         AS 'sandbox.Rational.newInstance'
  11.         LANGUAGE JAVA IMMUTABLE STRICT;
  12.  
  13.   CREATE FUNCTION javatest.rational_as_double(javatest.rational) RETURNS float8
  14.         AS 'sandbox.Rational.value'
  15.         LANGUAGE JAVA IMMUTABLE STRICT;
  16.  
  17.   CREATE CAST (varchar AS javatest.rational)
  18.       WITH FUNCTION javatest.rational_string_as_rational(varchar)
  19.       AS ASSIGNMENT;
  20.  
  21.   CREATE CAST (int4 AS javatest.rational)
  22.       WITH FUNCTION javatest.rational_int_as_rational(int4)
  23.       AS ASSIGNMENT;
  24.  
  25.   CREATE CAST (int8 AS javatest.rational)
  26.       WITH FUNCTION javatest.rational_long_as_rational(int8)
  27.       AS ASSIGNMENT;
  28.  
  29.   CREATE CAST (javatest.rational AS float8)
  30.       WITH FUNCTION javatest.rational_as_double(javatest.rational)
  31.       AS ASSIGNMENT;
  CREATE FUNCTION javatest.rational_string_as_rational(varchar) RETURNS javatest.rational
        AS 'sandbox.Rational.newInstance'
        LANGUAGE JAVA IMMUTABLE STRICT;

  CREATE FUNCTION javatest.rational_int_as_rational(int4) RETURNS javatest.rational
        AS 'sandbox.Rational.newInstance'
        LANGUAGE JAVA IMMUTABLE STRICT;

  CREATE FUNCTION javatest.rational_long_as_rational(int8) RETURNS javatest.rational
        AS 'sandbox.Rational.newInstance'
        LANGUAGE JAVA IMMUTABLE STRICT;

  CREATE FUNCTION javatest.rational_as_double(javatest.rational) RETURNS float8
        AS 'sandbox.Rational.value'
        LANGUAGE JAVA IMMUTABLE STRICT;

  CREATE CAST (varchar AS javatest.rational)
      WITH FUNCTION javatest.rational_string_as_rational(varchar)
      AS ASSIGNMENT;

  CREATE CAST (int4 AS javatest.rational)
      WITH FUNCTION javatest.rational_int_as_rational(int4)
      AS ASSIGNMENT;

  CREATE CAST (int8 AS javatest.rational)
      WITH FUNCTION javatest.rational_long_as_rational(int8)
      AS ASSIGNMENT;

  CREATE CAST (javatest.rational AS float8)
      WITH FUNCTION javatest.rational_as_double(javatest.rational)
      AS ASSIGNMENT;

(Sidenote: STRICT means that the function will return NULL if any argument is NULL. This allows the database to make some optimizations.)

(Sidenote: we may only be able to use the IMMUTABLE flag if the java objects are also immutable. We should probably make our Rational objects immutable since the other numeric types are immutable.)

Aggregate Functions

What about min()? Rational numbers are a numeric type so shouldn’t they support all of the standard aggregate functions?

Defining new aggregate functions is straightforward. Simple aggregate functions only need a static member function that take two UDT values and return one. This is easy to see with maximums, minimums, sums, products, etc. More complex aggregates require an ancillary UDT that contains state information, a static method that takes one state UDT and one UDT and returns a state UDT, and a finalization method that takes the final state UDT and produces the results. This is easy to see with averages – you need a state type that contains a counter and a running sum.

Several examples of the former type of aggregate function follow.

  1.     // compare two Rational objects. We use BigInteger to avoid overflow.
  2.     public static int compare(Rational p, Rational q) {
  3.         if (p == null) {
  4.             return 1;
  5.         } else if (q == null) {
  6.             return -1;
  7.         }
  8.         BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
  9.         BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
  10.         return l.compareTo(r);
  11.     }
  12.  
  13.     public static Rational min(Rational p, Rational q) {
  14.         if ((p == null) || (q == null)) {
  15.             return null;
  16.         }
  17.         return (p.compareTo(q) <= 0) ? p : q;
  18.     }
  19.  
  20.     public static Rational max(Rational p, Rational q) {
  21.         if ((p == null) || (q == null)) {
  22.             return null;
  23.         }
  24.         return (q.compareTo(p) < 0) ? p : q;
  25.     }
  26.  
  27.     public static Rational add(Rational p, Rational q) throws SQLException {
  28.         if ((p == null) || (q == null)) {
  29.             return null;
  30.         }
  31.         BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
  32.                 BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
  33.         BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
  34.         BigInteger gcd = n.gcd(d);
  35.         n = n.divide(gcd);
  36.         d = d.divide(gcd);
  37.         return new Rational(n.longValue(), d.longValue());
  38.     }
    // compare two Rational objects. We use BigInteger to avoid overflow.
    public static int compare(Rational p, Rational q) {
        if (p == null) {
            return 1;
        } else if (q == null) {
            return -1;
        }
        BigInteger l = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger r = BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator()));
        return l.compareTo(r);
    }

    public static Rational min(Rational p, Rational q) {
        if ((p == null) || (q == null)) {
            return null;
        }
        return (p.compareTo(q) <= 0) ? p : q;
    }

    public static Rational max(Rational p, Rational q) {
        if ((p == null) || (q == null)) {
            return null;
        }
        return (q.compareTo(p) < 0) ? p : q;
    }

    public static Rational add(Rational p, Rational q) throws SQLException {
        if ((p == null) || (q == null)) {
            return null;
        }
        BigInteger n = BigInteger.valueOf(p.getNumerator()).multiply(BigInteger.valueOf(q.getDenominator())).add(
                BigInteger.valueOf(q.getNumerator()).multiply(BigInteger.valueOf(p.getDenominator())));
        BigInteger d = BigInteger.valueOf(p.getDenominator()).multiply(BigInteger.valueOf(q.getDenominator()));
        BigInteger gcd = n.gcd(d);
        n = n.divide(gcd);
        d = d.divide(gcd);
        return new Rational(n.longValue(), d.longValue());
    }

and

  1.     CREATE FUNCTION javatest.min(javatest.rational, javatest.rational) RETURNS javatest.rational
  2.         AS 'sandbox.Rational.min'
  3.         LANGUAGE JAVA IMMUTABLE STRICT;
  4.  
  5.     CREATE FUNCTION javatest.max(javatest.rational, javatest.rational) RETURNS javatest.rational
  6.         AS 'sandbox.Rational.max'
  7.         LANGUAGE JAVA IMMUTABLE STRICT;
  8.  
  9.     CREATE AGGREGATE min(javatest.rational) (
  10.       sfunc = javatest.min,
  11.       stype = javatest.rational
  12.     );
  13.  
  14.     CREATE AGGREGATE max(javatest.rational) (
  15.       sfunc = javatest.max,
  16.       stype = javatest.rational
  17.     );
  18.  
  19.     CREATE AGGREGATE sum(javatest.rational) (
  20.       sfunc = javatest.add,
  21.       stype = javatest.rational
  22.     );
    CREATE FUNCTION javatest.min(javatest.rational, javatest.rational) RETURNS javatest.rational
        AS 'sandbox.Rational.min'
        LANGUAGE JAVA IMMUTABLE STRICT;

    CREATE FUNCTION javatest.max(javatest.rational, javatest.rational) RETURNS javatest.rational
        AS 'sandbox.Rational.max'
        LANGUAGE JAVA IMMUTABLE STRICT;

    CREATE AGGREGATE min(javatest.rational) (
      sfunc = javatest.min,
      stype = javatest.rational
    );

    CREATE AGGREGATE max(javatest.rational) (
      sfunc = javatest.max,
      stype = javatest.rational
    );

    CREATE AGGREGATE sum(javatest.rational) (
      sfunc = javatest.add,
      stype = javatest.rational
    );

Integration with Hibernate

It is possible to link PL/Java user-defined types and Hibernate user-defined types. Warning: the hibernate code is highly database-specific.

This is the hibernate user-defined type. PostgreSQL 9.1 does not support the STRUCT type and uses strings instead. We don’t have to use the PL/Java user-defined data type to perform the marshaling but it ensures consistency. TheDbRationalType is the Rational class above. The same class could be used in both places but would introduce dependency on a Hibernate interface into the PL/Java class. This may be acceptable if you extract that single interface from the Hibernate source code.

  1. public class Rational implements UserType, Serializable {
  2.     private final int[] sqlTypesSupported = new int[] { Types.OTHER };
  3.     private long numerator;
  4.     private long denominator;
  5.  
  6.     public Rational() {
  7.         numerator = 0;
  8.         denominator = 1;
  9.     }
  10.  
  11.     public Rational(long numerator, long denominator) {
  12.         this.numerator = numerator;
  13.         this.denominator = denominator;
  14.     }
  15.  
  16.     public long getNumerator() {
  17.         return numerator;
  18.     }
  19.  
  20.     public long getDenominator() {
  21.         return denominator;
  22.     }
  23.  
  24.     @Override
  25.     public Object assemble(Serializable cached, Object owner)
  26.             throws HibernateException {
  27.         if (!(cached instanceof Rational)) {
  28.             throw new HibernateException("invalid argument");
  29.         }
  30.         Rational r = (Rational) cached;
  31.         return new Rational(r.getNumerator(), r.getDenominator());
  32.     }
  33.  
  34.     @Override
  35.     public Serializable disassemble(Object value) throws HibernateException {
  36.         if (!(value instanceof Rational)) {
  37.             throw new HibernateException("invalid argument");
  38.         }
  39.         return (Rational) value;
  40.     }
  41.  
  42.     @Override
  43.     public Object deepCopy(Object value) throws HibernateException {
  44.         if (value == null) {
  45.             return null
  46.         }
  47.         if (!(value instanceof Rational)) {
  48.             throw new HibernateException("invalid argument");
  49.         }
  50.         Rational v = (Rational) value;
  51.         return new Rational(v.getNumerator(), v.getDenominator());
  52.     }
  53.  
  54.     @Override
  55.     public boolean isMutable() {
  56.         return true;
  57.     }
  58.  
  59.     //
  60.     // important: PGobject is postgresql-specific
  61.     //
  62.     @Override
  63.     public Object nullSafeGet(ResultSet rs, String[] names, Object owners)
  64.             throws HibernateException, SQLException {
  65.         PGobject pgo = (PGobject) rs.getObject(names[0]);
  66.         if (rs.wasNull()) {
  67.             return null;
  68.         }
  69.         TheDbRationalType r = TheDbRationalType.parse(pgo.getValue(), "rational");
  70.         return new Rational(r.getNumerator(), r.getDenominator());
  71.     }
  72.  
  73.     //
  74.     // important: using Types.OTHER may be postgresql-specific
  75.     //
  76.     @Override
  77.     public void nullSafeSet(PreparedStatement ps, Object value, int index)
  78.             throws HibernateException, SQLException {
  79.         if (value == null) {
  80.             ps.setNull(index, Types.OTHER);
  81.         } else if (!(value instanceof Rational)) {
  82.             throw new HibernateException("invalid argument");
  83.         } else {
  84.             Rational t = (Rational) value;
  85.             ps.setObject(index,
  86.                     new TheDbRationalType(t.getNumerator(), t.getDenominator()), Types.OTHER);
  87.         }
  88.     }
  89.  
  90.     @Override
  91.     public Object replace(Object original, Object target, Object owner)
  92.             throws HibernateException {
  93.         if (!(original instanceof Rational)
  94.                 || !(target instanceof Rational)) {
  95.             throw new HibernateException("invalid argument");
  96.         }
  97.         Rational r = (Rational) original;
  98.         return new Rational(r.getNumerator(), r.getDenominator());
  99.     }
  100.  
  101.     @Override
  102.     public Class returnedClass() {
  103.         return Rational.class;
  104.     }
  105.  
  106.     @Override
  107.     public int[] sqlTypes() {
  108.         return sqlTypesSupported;
  109.     }
  110.  
  111.     @Override
  112.     public String toString() {
  113.         String value = "";
  114.         if (denominator == 1) {
  115.             value = String.valueOf(numerator);
  116.         } else {
  117.             value = String.format("%d/%d", numerator, denominator);
  118.         }
  119.         return value;
  120.     }
  121.  
  122.     // for UserType
  123.     @Override
  124.     public int hashCode(Object value) {
  125.         Rational r = (Rational) value;
  126.         return (int) (31 * r.getNumerator() + r.getDenominator());
  127.     }
  128.  
  129.     @Override
  130.     public int hashCode() {
  131.         return hashCode(this);
  132.     }
  133.  
  134.     // for UserType
  135.     @Override
  136.     public boolean equals(Object left, Object right) {
  137.         if (left == right) {
  138.             return true;
  139.         }
  140.         if ((left == null) || (right == null)) {
  141.             return false;
  142.         }
  143.         if (!(left instanceof Rational) || !(right instanceof Rational)) {
  144.             return false;
  145.         }
  146.  
  147.         Rational l = (Rational) left;
  148.         Rational r = (Rational) right;
  149.         return (l.getNumerator() == r.getNumerator())
  150.                 && (l.getDenominator() == r.getDenominator());
  151.     }
  152.  
  153.     @Override
  154.     public boolean equals(Object value) {
  155.         return equals(this, value);
  156.     }
  157. }
public class Rational implements UserType, Serializable {
    private final int[] sqlTypesSupported = new int[] { Types.OTHER };
    private long numerator;
    private long denominator;

    public Rational() {
        numerator = 0;
        denominator = 1;
    }

    public Rational(long numerator, long denominator) {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public long getNumerator() {
        return numerator;
    }

    public long getDenominator() {
        return denominator;
    }

    @Override
    public Object assemble(Serializable cached, Object owner)
            throws HibernateException {
        if (!(cached instanceof Rational)) {
            throw new HibernateException("invalid argument");
        }
        Rational r = (Rational) cached;
        return new Rational(r.getNumerator(), r.getDenominator());
    }

    @Override
    public Serializable disassemble(Object value) throws HibernateException {
        if (!(value instanceof Rational)) {
            throw new HibernateException("invalid argument");
        }
        return (Rational) value;
    }

    @Override
    public Object deepCopy(Object value) throws HibernateException {
        if (value == null) {
            return null
        }
        if (!(value instanceof Rational)) {
            throw new HibernateException("invalid argument");
        }
        Rational v = (Rational) value;
        return new Rational(v.getNumerator(), v.getDenominator());
    }

    @Override
    public boolean isMutable() {
        return true;
    }

    //
    // important: PGobject is postgresql-specific
    //
    @Override
    public Object nullSafeGet(ResultSet rs, String[] names, Object owners)
            throws HibernateException, SQLException {
        PGobject pgo = (PGobject) rs.getObject(names[0]);
        if (rs.wasNull()) {
            return null;
        }
        TheDbRationalType r = TheDbRationalType.parse(pgo.getValue(), "rational");
        return new Rational(r.getNumerator(), r.getDenominator());
    }

    //
    // important: using Types.OTHER may be postgresql-specific
    //
    @Override
    public void nullSafeSet(PreparedStatement ps, Object value, int index)
            throws HibernateException, SQLException {
        if (value == null) {
            ps.setNull(index, Types.OTHER);
        } else if (!(value instanceof Rational)) {
            throw new HibernateException("invalid argument");
        } else {
            Rational t = (Rational) value;
            ps.setObject(index,
                    new TheDbRationalType(t.getNumerator(), t.getDenominator()), Types.OTHER);
        }
    }

    @Override
    public Object replace(Object original, Object target, Object owner)
            throws HibernateException {
        if (!(original instanceof Rational)
                || !(target instanceof Rational)) {
            throw new HibernateException("invalid argument");
        }
        Rational r = (Rational) original;
        return new Rational(r.getNumerator(), r.getDenominator());
    }

    @Override
    public Class returnedClass() {
        return Rational.class;
    }

    @Override
    public int[] sqlTypes() {
        return sqlTypesSupported;
    }

    @Override
    public String toString() {
        String value = "";
        if (denominator == 1) {
            value = String.valueOf(numerator);
        } else {
            value = String.format("%d/%d", numerator, denominator);
        }
        return value;
    }

    // for UserType
    @Override
    public int hashCode(Object value) {
        Rational r = (Rational) value;
        return (int) (31 * r.getNumerator() + r.getDenominator());
    }

    @Override
    public int hashCode() {
        return hashCode(this);
    }

    // for UserType
    @Override
    public boolean equals(Object left, Object right) {
        if (left == right) {
            return true;
        }
        if ((left == null) || (right == null)) {
            return false;
        }
        if (!(left instanceof Rational) || !(right instanceof Rational)) {
            return false;
        }

        Rational l = (Rational) left;
        Rational r = (Rational) right;
        return (l.getNumerator() == r.getNumerator())
                && (l.getDenominator() == r.getDenominator());
    }

    @Override
    public boolean equals(Object value) {
        return equals(this, value);
    }
}

CustomTypes.hbm.xml

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <!DOCTYPE hibernate-mapping PUBLIC
  3.        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  4.        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
  5.  
  6. <hibernate-mapping>
  7.  
  8.     <typedef name="javatest.rational" class="sandbox.RationalType"/>
  9.  
  10. </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <typedef name="javatest.rational" class="sandbox.RationalType"/>

</hibernate-mapping>

TestTable.hbm.xml

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <!DOCTYPE hibernate-mapping PUBLIC
  3.        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
  4.        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
  5.  
  6. <hibernate-mapping>
  7.  
  8.     <class name="sandbox.TestTable" table="test_table">
  9.         <id name="id"/>
  10.         <property name="value" type="javatest.rational" />
  11.     </class>
  12.  
  13. </hibernate-mapping>
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">

<hibernate-mapping>

    <class name="sandbox.TestTable" table="test_table">
        <id name="id"/>
        <property name="value" type="javatest.rational" />
    </class>

</hibernate-mapping>

More Information

Creating a Scalar UDT in Java (user guide)

CREATE AGGREGATE documentation (PostgreSQL)

CREATE CAST documentation (PostgreSQL)

CREATE TYPE documentation (PostgreSQL)

CREATE OPERATOR documentation (PostgreSQL) (We’ll discuss this next time – it’s how you define tests for equality, etc.)

CREATE OPERATOR CLASS documentation (PostgreSQL) (We’ll discuss this next time – it’s how you define indexes.)

Interfacing user-defined types to indexes (PostgreSQL) (We’ll discuss this next time.)

java

Introduction to PostgreSQL PL/Java, part 3: Triggers

By thebearinboulderNo Comments

In the past two entries I discussed the basics of PL/Java. Here I will describe one of the most powerful uses of stored procedures – triggers.

Triggers

A database trigger is stored procedure that is automatically run during one of the three of the four CRUD (create-read-update-delete) operations.

  • insertion - the trigger is provided the new value and is able to modify the values or prohibit the operation outright.
  • update - the trigger is provided both old and new values. Again it is able to modify the values or prohibit the operation.
  • deletion - the trigger is provided the old value. It is not able to modify the value but can prohibit the operation.

A trigger can be run before or after the operation. You would execute a trigger before an operation if you want to modify the values; you would execute it after an operation if you want to log the results.

Typical Usage

Insertion and Update: Data Validation

A pre-trigger on insert and update operations can be used to enforce data integrity and consistency. In this case the results are either accepted or the operation is prohibited.

Insertion and Update: Data Normalization and Sanitization

Sometimes values can have multiple representations or potentially be dangerous. A pre-trigger is a chance to clean up the data, e.g., to tidy up XML or replace < with &lt; and > with &gt;.

All Operations: Audit Logging

A post-trigger on all operations can be used to enforce audit logging. Applications can log their own actions but can’t log direct access to the database. This is a solution to this problem.

A trigger can be run for each row or after completion of an entire statement. Update triggers can also be conditional.

Triggers can be used to create ‘updateable views’.

PL/Java Implementation

Any java method can be a used in a trigger provided it is a public static method returning void that takes a single argument, a TriggerData object. Triggers can be called “ON EACH ROW” or “ON STATEMENT”.

TriggerDatas that are “ON EACH ROW” contain a single-row, read-only, ResultSet as the ‘old’ value on updates and deletions, and a single-row, updatable ResultSet as the ‘new’ value on insertions and updates. This can be used to modify content, log actions, etc.

  1. public class AuditTrigger {
  2.  
  3.     public static void auditFoobar(TriggerData td) throws SQLException {
  4.  
  5.         Connection conn = DriverManager
  6.                 .getConnection("jdbc:default:connection");
  7.         PreparedStatement ps = conn
  8.                 .prepareStatement("insert into javatest.foobar_audit(what, whenn, data) values (?, ?, ?::xml)");
  9.  
  10.         if (td.isFiredByInsert()) {
  11.             ps.setString(1, "INSERT");
  12.         } else if (td.isFiredByUpdate()) {
  13.             ps.setString(1, "UPDATE");
  14.         } else if (td.isFiredByDelete()) {
  15.             ps.setString(1, "DELETE");
  16.         }
  17.         ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
  18.  
  19.         ResultSet rs = td.getNew();
  20.         if (rs != null) {
  21.             ps.setString(3, toXml(rs));
  22.         } else {
  23.             ps.setNull(3, Types.VARCHAR);
  24.         }
  25.  
  26.         ps.execute();
  27.         ps.close();
  28.     }
  29.  
  30.     // simple marshaler. We could use jaxb or similar library
  31.     static String toXml(ResultSet rs) throws SQLException {
  32.         String foo = rs.getString(1);
  33.         if (rs.wasNull()) {
  34.             foo = "";
  35.         }
  36.         String bar = rs.getString(2);
  37.         if (rs.wasNull()) {
  38.             bar = "";
  39.         }
  40.         return String.format("<my-class><foo>%s</foo><bar>%s</bar></my-class>", foo, bar);
  41.     }
  42. }
public class AuditTrigger {

    public static void auditFoobar(TriggerData td) throws SQLException {

        Connection conn = DriverManager
                .getConnection("jdbc:default:connection");
        PreparedStatement ps = conn
                .prepareStatement("insert into javatest.foobar_audit(what, whenn, data) values (?, ?, ?::xml)");

        if (td.isFiredByInsert()) {
            ps.setString(1, "INSERT");
        } else if (td.isFiredByUpdate()) {
            ps.setString(1, "UPDATE");
        } else if (td.isFiredByDelete()) {
            ps.setString(1, "DELETE");
        }
        ps.setTimestamp(2, new Timestamp(System.currentTimeMillis()));

        ResultSet rs = td.getNew();
        if (rs != null) {
            ps.setString(3, toXml(rs));
        } else {
            ps.setNull(3, Types.VARCHAR);
        }

        ps.execute();
        ps.close();
    }

    // simple marshaler. We could use jaxb or similar library
    static String toXml(ResultSet rs) throws SQLException {
        String foo = rs.getString(1);
        if (rs.wasNull()) {
            foo = "";
        }
        String bar = rs.getString(2);
        if (rs.wasNull()) {
            bar = "";
        }
        return String.format("<my-class><foo>%s</foo><bar>%s</bar></my-class>", foo, bar);
    }
}

and

  1.   CREATE TABLE javatest.foobar (
  2.        foo   varchar(10),
  3.        bar   varchar(10)
  4.   );
  5.  
  6.   CREATE TABLE javatest.foobar_audit (
  7.        what  varchar(10) not null,
  8.        whenn timestamp not null,
  9.        data  xml
  10.   );
  11.  
  12.   CREATE FUNCTION javatest.audit_foobar()
  13.       RETURNS trigger
  14.       AS 'sandbox.AuditTrigger.auditFoobar'
  15.       LANGUAGE 'java';
  16.  
  17.   CREATE TRIGGER foobar_audit
  18.       AFTER INSERT OR UPDATE OR DELETE ON javatest.foobar
  19.       FOR EACH ROW
  20.       EXECUTE PROCEDURE javatest.audit_foobar();
  CREATE TABLE javatest.foobar (
       foo   varchar(10),
       bar   varchar(10)
  );

  CREATE TABLE javatest.foobar_audit (
       what  varchar(10) not null,
       whenn timestamp not null,
       data  xml
  );

  CREATE FUNCTION javatest.audit_foobar()
      RETURNS trigger
      AS 'sandbox.AuditTrigger.auditFoobar'
      LANGUAGE 'java';

  CREATE TRIGGER foobar_audit
      AFTER INSERT OR UPDATE OR DELETE ON javatest.foobar
      FOR EACH ROW
      EXECUTE PROCEDURE javatest.audit_foobar();

Rules

A PostgreSQL extension is Rules. They are similar to triggers but a bit more flexible. One important difference is that Rules can be triggered on a SELECT statement, not just INSERT, UPDATE and DELETE.

Rules, unlike triggers, use standard functions.

The Interface

As before I have not been able to find a maven repository of a recent version and am including the files for your convenience.

TriggerData

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root folder of this project or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11.  
  12. /**
  13.  * The SQL 2003 spec. does not stipulate a standard way of mapping
  14.  * triggers to functions. The PLJava mapping use this interface. All
  15.  * functions that are intended to be triggers must be public, static,
  16.  * return void, and take a <code>TriggerData</code> as their argument.
  17.  *
  18.  * @author Thomas Hallgren
  19.  */
  20. public interface TriggerData
  21. {
  22.     /**
  23.      * Returns the ResultSet that represents the new row. This ResultSet will
  24.      * be null for delete triggers and for triggers that was fired for
  25.      * statement.
  26. The returned set will be updateable and positioned on a
  27.      * valid row. When the trigger call returns, the trigger manager will see
  28.      * the changes that has been made to this row and construct a new tuple
  29.      * which will become the new or updated row.
  30.      *
  31.      * @return An updateable <code>ResultSet</code> containing one row or
  32.      *         <code>null</code>.
  33.      * @throws SQLException
  34.      *             if the contained native buffer has gone stale.
  35.      */
  36.     ResultSet getNew() throws SQLException;
  37.  
  38.     /**
  39.      * Returns the ResultSet that represents the old row. This ResultSet will
  40.      * be null for insert triggers and for triggers that was fired for
  41.      * statement.
  42. The returned set will be read-only and positioned on a
  43.      * valid row.
  44.      *
  45.      * @return A read-only <code>ResultSet</code> containing one row or
  46.      *         <code>null</code>.
  47.      * @throws SQLException
  48.      *             if the contained native buffer has gone stale.
  49.      */
  50.     ResultSet getOld() throws SQLException;
  51.  
  52.     /**
  53.      * Returns the arguments for this trigger (as declared in the <code>CREATE TRIGGER</code>
  54.      * statement. If the trigger has no arguments, this method will return an
  55.      * array with size 0.
  56.      *
  57.      * @throws SQLException
  58.      *             if the contained native buffer has gone stale.
  59.      */
  60.     String[] getArguments() throws SQLException;
  61.  
  62.     /**
  63.      * Returns the name of the trigger (as declared in the <code>CREATE TRIGGER</code>
  64.      * statement).
  65.      *
  66.      * @throws SQLException
  67.      *             if the contained native buffer has gone stale.
  68.      */
  69.     String getName() throws SQLException;
  70.  
  71.     /**
  72.      * Returns the name of the table for which this trigger was created (as
  73.      * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getTableName() throws SQLException; /** * Returns the name of the schema of the table for which this trigger was created (as * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getSchemaName() throws SQLException; /** * Returns <code>true</code> if the trigger was fired after the statement * or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredAfter() throws SQLException; /** * Returns <code>true</code> if the trigger was fired before the * statement or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredBefore() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for each row * (as opposed to once for the entire statement). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForEachRow() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for the entire * statement (as opposed to once for each row). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForStatement() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by a <code>DELETE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByDelete() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>INSERT</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByInsert() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>UPDATE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByUpdate() throws SQLException; } </code></code>
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * The SQL 2003 spec. does not stipulate a standard way of mapping
 * triggers to functions. The PLJava mapping use this interface. All
 * functions that are intended to be triggers must be public, static,
 * return void, and take a <code>TriggerData</code> as their argument.
 *
 * @author Thomas Hallgren
 */
public interface TriggerData
{
	/**
	 * Returns the ResultSet that represents the new row. This ResultSet will
	 * be null for delete triggers and for triggers that was fired for
	 * statement.
The returned set will be updateable and positioned on a
	 * valid row. When the trigger call returns, the trigger manager will see
	 * the changes that has been made to this row and construct a new tuple
	 * which will become the new or updated row.
	 *
	 * @return An updateable <code>ResultSet</code> containing one row or
	 *         <code>null</code>.
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	ResultSet getNew() throws SQLException;

	/**
	 * Returns the ResultSet that represents the old row. This ResultSet will
	 * be null for insert triggers and for triggers that was fired for
	 * statement.
The returned set will be read-only and positioned on a
	 * valid row.
	 *
	 * @return A read-only <code>ResultSet</code> containing one row or
	 *         <code>null</code>.
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	ResultSet getOld() throws SQLException;

	/**
	 * Returns the arguments for this trigger (as declared in the <code>CREATE TRIGGER</code>
	 * statement. If the trigger has no arguments, this method will return an
	 * array with size 0.
	 *
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	String[] getArguments() throws SQLException;

	/**
	 * Returns the name of the trigger (as declared in the <code>CREATE TRIGGER</code>
	 * statement).
	 *
	 * @throws SQLException
	 *             if the contained native buffer has gone stale.
	 */
	String getName() throws SQLException;

	/**
	 * Returns the name of the table for which this trigger was created (as
	 * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getTableName() throws SQLException; /** * Returns the name of the schema of the table for which this trigger was created (as * declared in the <code>CREATE TRIGGER</code statement). * * @throws SQLException * if the contained native buffer has gone stale. */ String getSchemaName() throws SQLException; /** * Returns <code>true</code> if the trigger was fired after the statement * or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredAfter() throws SQLException; /** * Returns <code>true</code> if the trigger was fired before the * statement or row action that it is associated with. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredBefore() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for each row * (as opposed to once for the entire statement). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForEachRow() throws SQLException; /** * Returns <code>true</code> if this trigger is fired once for the entire * statement (as opposed to once for each row). * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredForStatement() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by a <code>DELETE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByDelete() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>INSERT</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByInsert() throws SQLException; /** * Returns <code>true</code> if this trigger was fired by an <code>UPDATE</code>. * * @throws SQLException * if the contained native buffer has gone stale. */ boolean isFiredByUpdate() throws SQLException; } </code></code>

TriggerException

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root folder of this project or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.SQLException;
  10.  
  11. /**
  12.  * An exception specially suited to be thrown from within a method
  13.  * designated to be a trigger function. The message generated by
  14.  * this exception will contain information on what trigger and
  15.  * what relation it was that caused the exception
  16.  *
  17.  * @author Thomas Hallgren
  18.  */
  19. public class TriggerException extends SQLException
  20. {
  21.     private static final long serialVersionUID = 5543711707414329116L;
  22.  
  23.     private static boolean s_recursionLock = false;
  24.  
  25.     public static final String TRIGGER_ACTION_EXCEPTION = "09000";
  26.  
  27.     private static final String makeMessage(TriggerData td, String message)
  28.     {
  29.         StringBuffer bld = new StringBuffer();
  30.         bld.append("In Trigger ");
  31.         if(!s_recursionLock)
  32.         {
  33.             s_recursionLock = true;
  34.             try
  35.             {
  36.                 bld.append(td.getName());
  37.                 bld.append(" on relation ");
  38.                 bld.append(td.getTableName());
  39.             }
  40.             catch(SQLException e)
  41.             {
  42.                 bld.append("(exception while generating exception message)");
  43.             }
  44.             finally
  45.             {
  46.                 s_recursionLock = false;
  47.             }
  48.         }
  49.         if(message != null)
  50.         {
  51.             bld.append(": ");
  52.             bld.append(message);
  53.         }
  54.         return bld.toString();
  55.     }
  56.  
  57.     /**
  58.      * Create an exception based on the <code>TriggerData</code> that was
  59.      * passed to the trigger method.
  60.      * @param td The <code>TriggerData</code> that was passed to the trigger
  61.      * method.
  62.      */
  63.     public TriggerException(TriggerData td)
  64.     {
  65.         super(makeMessage(td, null), TRIGGER_ACTION_EXCEPTION);
  66.     }
  67.  
  68.     /**
  69.      * Create an exception based on the <code>TriggerData</code> that was
  70.      * passed to the trigger method and an additional message.
  71.      * @param td The <code>TriggerData</code> that was passed to the trigger
  72.      * method.
  73.      * @param reason An additional message with info about the exception.
  74.      */
  75.     public TriggerException(TriggerData td, String reason)
  76.     {
  77.         super(makeMessage(td, reason), TRIGGER_ACTION_EXCEPTION);
  78.     }
  79. }
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.SQLException;

/**
 * An exception specially suited to be thrown from within a method
 * designated to be a trigger function. The message generated by
 * this exception will contain information on what trigger and
 * what relation it was that caused the exception
 *
 * @author Thomas Hallgren
 */
public class TriggerException extends SQLException
{
    private static final long serialVersionUID = 5543711707414329116L;

    private static boolean s_recursionLock = false;

    public static final String TRIGGER_ACTION_EXCEPTION = "09000";

    private static final String makeMessage(TriggerData td, String message)
    {
        StringBuffer bld = new StringBuffer();
        bld.append("In Trigger ");
        if(!s_recursionLock)
        {
            s_recursionLock = true;
            try
            {
                bld.append(td.getName());
                bld.append(" on relation ");
                bld.append(td.getTableName());
            }
            catch(SQLException e)
            {
                bld.append("(exception while generating exception message)");
            }
            finally
            {
                s_recursionLock = false;
            }
        }
        if(message != null)
        {
            bld.append(": ");
            bld.append(message);
        }
        return bld.toString();
    }

    /**
     * Create an exception based on the <code>TriggerData</code> that was
     * passed to the trigger method.
     * @param td The <code>TriggerData</code> that was passed to the trigger
     * method.
     */
    public TriggerException(TriggerData td)
    {
        super(makeMessage(td, null), TRIGGER_ACTION_EXCEPTION);
    }

    /**
     * Create an exception based on the <code>TriggerData</code> that was
     * passed to the trigger method and an additional message.
     * @param td The <code>TriggerData</code> that was passed to the trigger
     * method.
     * @param reason An additional message with info about the exception.
     */
    public TriggerException(TriggerData td, String reason)
    {
        super(makeMessage(td, reason), TRIGGER_ACTION_EXCEPTION);
    }
}

More Information

PostgreSQL ‘create trigger’ documentation.

PostgreSQL ‘create rule’ documentation.

java

Introduction to PostgreSQL PL/Java, part 2: Working with Lists

By thebearinboulder1 Comment

Last time I discussed the basics of working with PL/Java. This time I’ll discuss working with lists.

Lists of Scalar Values

Lists of scalar values are returned as Iterators in the java world and SETOF in the SQL world.

  1.     public static Iterator<String> colors() {
  2.         List<String> colors = Arrays.asList("red", "green", "blue");
  3.         return colors.iterator();
  4.     }
    public static Iterator<String> colors() {
        List<String> colors = Arrays.asList("red", "green", "blue");
        return colors.iterator();
    }

and

  1.   CREATE FUNCTION javatest.colors()
  2.       RETURNS SETOF varchar
  3.       AS 'sandbox.PLJava.colors'
  4.       IMMUTABLE LANGUAGE java;
  CREATE FUNCTION javatest.colors()
      RETURNS SETOF varchar
      AS 'sandbox.PLJava.colors'
      IMMUTABLE LANGUAGE java;

I’ve added the IMMUTABLE keyword since this function will always return the same values. This allows the database to perform caching and query optimization.

You don’t need to know the results, or even the size of the results, before you start. Following is a sequence that’s believed to always terminate but this hasn’t been proven. (Unfortunately I’ve forgotten the name of the sequence.) As a sidenote this isn’t a complete solution since it doesn’t check for overflows – a correct implemention should either check this or use BigInteger.

  1.     public static Iterator seq(int start) {
  2.         Iterator iter = null;
  3.         try {
  4.             iter = new SeqIterator(start);
  5.         } catch (IllegalArgumentException e) {
  6.             // should log error...
  7.         }
  8.         return iter;
  9.     }
  10.  
  11.     public static class SeqIterator implements Iterator {
  12.         private int next;
  13.         private boolean done = false;
  14.  
  15.         public SeqIterator(int start) {
  16.             if (start <= 0) {
  17.                 throw new IllegalArgumentException();
  18.             }
  19.             this.next = start;
  20.         }
  21.  
  22.         @Override
  23.         public boolean hasNext() {
  24.             return !done;
  25.         }
  26.  
  27.         @Override
  28.         public Integer next() {
  29.             int value = next;
  30.             next = (next % 2 == 0) ? next / 2 : 3 * next + 1;
  31.             done = (value == 1);
  32.             return value;
  33.         }
  34.  
  35.         @Override
  36.         public void remove() {
  37.             throw new UnsupportedOperationException();
  38.         }
  39.     }
    public static Iterator seq(int start) {
        Iterator iter = null;
        try {
            iter = new SeqIterator(start);
        } catch (IllegalArgumentException e) {
            // should log error...
        }
        return iter;
    }

    public static class SeqIterator implements Iterator {
        private int next;
        private boolean done = false;

        public SeqIterator(int start) {
            if (start <= 0) {
                throw new IllegalArgumentException();
            }
            this.next = start;
        }

        @Override
        public boolean hasNext() {
            return !done;
        }

        @Override
        public Integer next() {
            int value = next;
            next = (next % 2 == 0) ? next / 2 : 3 * next + 1;
            done = (value == 1);
            return value;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

and

  1.   CREATE FUNCTION javatest.seq(int)
  2.       RETURNS SETOF int
  3.       AS 'sandbox.PLJava.seq'
  4.       IMMUTABLE LANGUAGE java;
  CREATE FUNCTION javatest.seq(int)
      RETURNS SETOF int
      AS 'sandbox.PLJava.seq'
      IMMUTABLE LANGUAGE java;

All things being equal it is better to create each result as needed. This usually reduces the memory footprint and avoids unnecessary work if the query has a LIMIT clause.

Single Tuples

A single tuple is returned in a ResultSet.

  1.     public static boolean singleWord(ResultSet receiver) throws SQLException {
  2.         receiver.updateString("English", "hello");
  3.         receiver.updateString("Spanish", "hola");
  4.         return true;
  5.     }
    public static boolean singleWord(ResultSet receiver) throws SQLException {
        receiver.updateString("English", "hello");
        receiver.updateString("Spanish", "hola");
        return true;
    }

and

  1.   CREATE TYPE word AS (
  2.       English varchar,
  3.       Spanish varchar);
  4.  
  5.   CREATE FUNCTION javatest.single_word()
  6.       RETURNS word
  7.       AS 'sandbox.PLJava.singleWord'
  8.       IMMUTABLE LANGUAGE java;
  CREATE TYPE word AS (
      English varchar,
      Spanish varchar);

  CREATE FUNCTION javatest.single_word()
      RETURNS word
      AS 'sandbox.PLJava.singleWord'
      IMMUTABLE LANGUAGE java;

A valid result is indicated by returning true, a null result is indicated by returning false. A complex type can be passed into a java method in the same manner – it is a read-only ResultSet containing a single row.

Lists of Tuples

Returning lists of complex values requires a class implementing one of two interfaces.

org.postgresql.pljava.ResultSetProvider

A ResultSetProvider is used when the results can be created programmatically or on an as-needed basis.

  1.    public static ResultSetProvider listWords() {
  2.         return new WordProvider();
  3.     }
  4.  
  5.     public static class WordProvider implements ResultSetProvider {
  6.         private final Map<String,String> words = new HashMap<String,String>();
  7.         private final Iterator<String> keys;
  8.  
  9.         public WordProvider() {
  10.             words.put("one", "uno");
  11.             words.put("two", "dos");
  12.             words.put("three", "tres");
  13.             words.put("four", "quatro");
  14.             keys = words.keySet().iterator();
  15.         }
  16.  
  17.         @Override
  18.         public boolean assignRowValues(ResultSet receiver, int currentRow)
  19.                 throws SQLException {
  20.             if (!keys.hasNext()) {
  21.                 return false;
  22.             }
  23.             String key = keys.next();
  24.             receiver.updateString("English", key);
  25.             receiver.updateString("Spanish", words.get(key));
  26.             return true;
  27.         }
  28.  
  29.         @Override
  30.         public void close() throws SQLException {
  31.         }
  32.     }
   public static ResultSetProvider listWords() {
        return new WordProvider();
    }

    public static class WordProvider implements ResultSetProvider {
        private final Map<String,String> words = new HashMap<String,String>();
        private final Iterator<String> keys;

        public WordProvider() {
            words.put("one", "uno");
            words.put("two", "dos");
            words.put("three", "tres");
            words.put("four", "quatro");
            keys = words.keySet().iterator();
        }

        @Override
        public boolean assignRowValues(ResultSet receiver, int currentRow)
                throws SQLException {
            if (!keys.hasNext()) {
                return false;
            }
            String key = keys.next();
            receiver.updateString("English", key);
            receiver.updateString("Spanish", words.get(key));
            return true;
        }

        @Override
        public void close() throws SQLException {
        }
    }

and

  1.     CREATE FUNCTION javatest.list_words()
  2.       RETURNS SETOF word
  3.       AS 'sandbox.PLJava.listWords'
  4.       IMMUTABLE LANGUAGE java;
    CREATE FUNCTION javatest.list_words()
      RETURNS SETOF word
      AS 'sandbox.PLJava.listWords'
      IMMUTABLE LANGUAGE java;

org.postgresql.pljava.ResultSetHandle

A ResultSetHandle is typically used when the method uses an internal query.

  1.     public static ResultSetHandle listUsers() {
  2.         return new UsersHandle();
  3.     }
  4.  
  5.     public static class UsersHandle implements ResultSetHandle {
  6.         private Statement stmt;
  7.  
  8.         @Override
  9.         public ResultSet getResultSet() throws SQLException {
  10.             stmt = DriverManager.getConnection("jdbc:default:connection").createStatement();
  11.             return stmt.executeQuery("SELECT * FROM pg_user");
  12.         }
  13.  
  14.         @Override
  15.         public void close() throws SQLException {
  16.             stmt.close();
  17.         }
  18.     }
    public static ResultSetHandle listUsers() {
        return new UsersHandle();
    }

    public static class UsersHandle implements ResultSetHandle {
        private Statement stmt;

        @Override
        public ResultSet getResultSet() throws SQLException {
            stmt = DriverManager.getConnection("jdbc:default:connection").createStatement();
            return stmt.executeQuery("SELECT * FROM pg_user");
        }

        @Override
        public void close() throws SQLException {
            stmt.close();
        }
    }

and

  1.   CREATE FUNCTION javatest.list_users()
  2.       RETURNS SETOF pg_user
  3.       AS 'sandbox.PLJava.listUsers'
  4.       LANGUAGE java;
  CREATE FUNCTION javatest.list_users()
      RETURNS SETOF pg_user
      AS 'sandbox.PLJava.listUsers'
      LANGUAGE java;

The Interfaces

I have been unable a recent copy of the pljava jar in a standard maven repository. My solution was to extract the interfaces from the PL/Java source tarball. They are provided here for your convenience.

ResultSetProvider

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root folder of this project or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11.  
  12. /**
  13.  * An implementation of this interface is returned from functions and procedures
  14.  * that are declared to return <code>SET OF</code> a complex type. Functions that
  15.  * return <code>SET OF</code> a simple type should simply return an
  16.  * {@link java.util.Iterator Iterator}.
  17.  * @author Thomas Hallgren
  18.  */
  19. public interface ResultSetProvider
  20. {
  21.     /**
  22.      * This method is called once for each row that should be returned from
  23.      * a procedure that returns a set of rows. The receiver
  24.      * is a {@link org.postgresql.pljava.jdbc.SingleRowWriter SingleRowWriter}
  25.      * writer instance that is used for capturing the data for the row.
  26.      * @param receiver Receiver of values for the given row.
  27.      * @param currentRow Row number. First call will have row number 0.
  28.      * @return <code>true</code> if a new row was provided, <code>false</code>
  29.      * if not (end of data).
  30.      * @throws SQLException
  31.      */
  32.     boolean assignRowValues(ResultSet receiver, int currentRow)
  33.     throws SQLException;
  34.  
  35.     /**
  36.      * Called after the last row has returned or when the query evaluator decides
  37.      * that it does not need any more rows.
  38.      */
  39.     void close()
  40.     throws SQLException;
  41. }
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root folder of this project or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * An implementation of this interface is returned from functions and procedures
 * that are declared to return <code>SET OF</code> a complex type. Functions that
 * return <code>SET OF</code> a simple type should simply return an
 * {@link java.util.Iterator Iterator}.
 * @author Thomas Hallgren
 */
public interface ResultSetProvider
{
	/**
	 * This method is called once for each row that should be returned from
	 * a procedure that returns a set of rows. The receiver
	 * is a {@link org.postgresql.pljava.jdbc.SingleRowWriter SingleRowWriter}
	 * writer instance that is used for capturing the data for the row.
	 * @param receiver Receiver of values for the given row.
	 * @param currentRow Row number. First call will have row number 0.
	 * @return <code>true</code> if a new row was provided, <code>false</code>
	 * if not (end of data).
	 * @throws SQLException
	 */
	boolean assignRowValues(ResultSet receiver, int currentRow)
	throws SQLException;

	/**
	 * Called after the last row has returned or when the query evaluator decides
	 * that it does not need any more rows.
	 */
	void close()
	throws SQLException;
}

ResultSetHandle

  1. /*
  2.  * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
  3.  * Distributed under the terms shown in the file COPYRIGHT
  4.  * found in the root directory of this distribution or at
  5.  * http://eng.tada.se/osprojects/COPYRIGHT.html
  6.  */
  7. package org.postgresql.pljava;
  8.  
  9. import java.sql.ResultSet;
  10. import java.sql.SQLException;
  11.  
  12. /**
  13.  * An implementation of this interface is returned from functions and procedures
  14.  * that are declared to return <code>SET OF</code> a complex type in the form
  15.  * of a {@link java.sql.ResultSet}. The primary motivation for this interface is
  16.  * that an implementation that returns a ResultSet must be able to close the
  17.  * connection and statement when no more rows are requested.
  18.  * @author Thomas Hallgren
  19.  */
  20. public interface ResultSetHandle
  21. {
  22.     /**
  23.      * An implementation of this method will probably execute a query
  24.      * and return the result of that query.
  25.      * @return The ResultSet that represents the rows to be returned.
  26.      * @throws SQLException
  27.      */
  28.     ResultSet getResultSet()
  29.     throws SQLException;
  30.  
  31.     /**
  32.      * Called after the last row has returned or when the query evaluator decides
  33.      * that it does not need any more rows.
  34.      */
  35.     void close()
  36.     throws SQLException;
  37. }
/*
 * Copyright (c) 2004, 2005, 2006 TADA AB - Taby Sweden
 * Distributed under the terms shown in the file COPYRIGHT
 * found in the root directory of this distribution or at
 * http://eng.tada.se/osprojects/COPYRIGHT.html
 */
package org.postgresql.pljava;

import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * An implementation of this interface is returned from functions and procedures
 * that are declared to return <code>SET OF</code> a complex type in the form
 * of a {@link java.sql.ResultSet}. The primary motivation for this interface is
 * that an implementation that returns a ResultSet must be able to close the
 * connection and statement when no more rows are requested.
 * @author Thomas Hallgren
 */
public interface ResultSetHandle
{
	/**
	 * An implementation of this method will probably execute a query
	 * and return the result of that query.
	 * @return The ResultSet that represents the rows to be returned.
	 * @throws SQLException
	 */
	ResultSet getResultSet()
	throws SQLException;

	/**
	 * Called after the last row has returned or when the query evaluator decides
	 * that it does not need any more rows.
	 */
	void close()
	throws SQLException;
}

More Information

CREATE TYPE (PostgreSQL)

java

Introduction to PostgreSQL PL/java, part 1

By thebearinboulderNo Comments

Modern databases allow stored procedures to be written in a variety of languages. One commonly implemented language is java.

N.B., this article discusses the PostgreSQL-specific java implementation. The details will vary with other databases but the concepts will be the same.

Installation of PL/Java

Installation of PL/Java on an Ubuntu system is straightforward. I will first create a new template, template_java, so I can still create databases without the pl/java extensions.

At the command line, assuming you are a database superuser, enter

  1. # apt-get install postgresql-9.1
  2. # apt-get install postgresql-9.1-pljava-gcj
  3.  
  4. $ createdb template_java
  5. $ psql -d template_java -c "update db_database set datistemplate='t' where datnam='template_java'"
  6. $ psql -d template_java -f /usr/share/postgresql-9.1-pljava/install.sql
# apt-get install postgresql-9.1
# apt-get install postgresql-9.1-pljava-gcj

$ createdb template_java
$ psql -d template_java -c "update db_database set datistemplate='t' where datnam='template_java'"
$ psql -d template_java -f /usr/share/postgresql-9.1-pljava/install.sql

Limitations

The prepackaged Ubuntu package uses the Gnu GCJ java implementation, not a standard OpenJDK or Sun implementation. GCJ compiles java source files to native object code instead of byte code. The most recent versions of PL/Java are “trusted” – they can be relied upon to stay within their sandbox. Among other things this means that you can’t access the filesystem on the server.

If you must break the trust there is a second language, ‘javaU’, that can be used. Untrusted functions can only be created a the database superuser.

More importantly this implementation is single-threaded. This is critical to keep in mind if you need to communicate to other servers.

Something to consider is whether you want to compile your own commonly used libraries with GCJ and load them into the PostgreSQL server as shared libraries. Shared libraries go in /usr/lib/postgresql/9.1/lib and I may have more to say about this later.

Quick verification

We can easily check our installation by writing a quick test function. Create a scratch database using template_java and enter the following SQL:

  1. CREATE FUNCTION getsysprop(VARCHAR) RETURNS VARCHAR
  2.   AS 'java.lang.System.getProperty'
  3.   LANGUAGE java;
  4.  
  5. SELECT getsysprop('user.home');
CREATE FUNCTION getsysprop(VARCHAR) RETURNS VARCHAR
  AS 'java.lang.System.getProperty'
  LANGUAGE java;

SELECT getsysprop('user.home');

You should get “/var/lib/postgresql” as a result.

Installing Our Own Methods

This is a nice start but we don’t really gain much if we can’t call our own methods. Fortunately it isn’t hard to add our own.

A simple PL/Java procedure is

  1. package sandbox;
  2.  
  3. public class PLJava {
  4.     public static String hello(String name) {
  5.         if (name == null) {
  6.             return null;
  7.         }
  8.  
  9.         return "Hello, " + name + "!";
  10.     }
  11. }
package sandbox;

public class PLJava {
    public static String hello(String name) {
        if (name == null) {
            return null;
        }

        return "Hello, " + name + "!";
    }
}

There are two simple rules for methods implementing PL/Java procedures:

  • they must be public static
  • they must return null if any parameter is null

That’s it.

Importing the java class into PostgreSQL server is simple. Let’s assume that the package classes are in /tmp/sandbox.jar and our java-enabled database is mydb. Our commands are then

  1. --
  2. -- load java library
  3. --
  4. -- parameters:
  5. --   url_path - where the library is located
  6. --   url_name - how the library is referred to later
  7. --   deploy   - should the deployment descriptor be used?
  8. --
  9. select sqlj.install_jar('file:///tmp/sandbox.jar', 'sandbox', true);
  10.  
  11. --
  12. -- set classpath to include new library.
  13. --
  14. -- parameters
  15. --   schema    - schema (or database) name
  16. --   classpath - colon-separated list of url_names.
  17. --
  18. select sqlj.set_classpath('mydb', 'sandbox');
  19.  
  20. -- -------------------
  21. -- other procedures --
  22. -- -------------------
  23.  
  24. --
  25. -- reload java library
  26. --
  27. select sqlj.replace_jar('file:///tmp/sandbox.jar', 'sandbox', true);
  28.  
  29. --
  30. -- remove java library
  31. --
  32. -- parameters:
  33. --   url_name - how the library is referred to later
  34. --   undeploy - should the deployment descriptor be used?
  35. --
  36. select sqlj.remove_jar('sandbox', true);
  37.  
  38. --
  39. -- list classpath
  40. --
  41. select sqlj.get_classpath('mydb');
  42.  
  43. --
--
-- load java library
--
-- parameters:
--   url_path - where the library is located
--   url_name - how the library is referred to later
--   deploy   - should the deployment descriptor be used?
--
select sqlj.install_jar('file:///tmp/sandbox.jar', 'sandbox', true);

--
-- set classpath to include new library.
--
-- parameters
--   schema    - schema (or database) name
--   classpath - colon-separated list of url_names.
--
select sqlj.set_classpath('mydb', 'sandbox');

-- -------------------
-- other procedures --
-- -------------------

--
-- reload java library
--
select sqlj.replace_jar('file:///tmp/sandbox.jar', 'sandbox', true);

--
-- remove java library
--
-- parameters:
--   url_name - how the library is referred to later
--   undeploy - should the deployment descriptor be used?
--
select sqlj.remove_jar('sandbox', true);

--
-- list classpath
--
select sqlj.get_classpath('mydb');

--

It is important to remember to set the classpath. Libraries are automatically removed from the classpath when they’re unloaded but they are NOT automatically added to the classpath when they’re installed.

We aren’t quite finished – we still need to tell the system about our new function.

  1. --
  2. -- create function
  3. --
  4. CREATE FUNCTION mydb.hello(varchar) RETURNS varchar
  5.   AS 'sandbox.PLJava.hello'
  6.   LANGUAGE java;
  7.  
  8. --
  9. -- drop this function
  10. --
  11. DROP FUNCTION mydb.hello(varchar);
  12.  
  13. --
--
-- create function
--
CREATE FUNCTION mydb.hello(varchar) RETURNS varchar
  AS 'sandbox.PLJava.hello'
  LANGUAGE java;

--
-- drop this function
--
DROP FUNCTION mydb.hello(varchar);

--

We can now call our java method in the same manner as any other stored procedures.

Deployment Descriptor

There’s a headache here – it’s necessary to explicitly create the functions when installing a library and dropping them when removing a library. This is time-consuming and error-prone in all but the simplest cases.

Fortunately there’s a solution to this problem – deployment descriptors. The precise format is defined by ISO/IEC 9075-13:2003 but a simple example should suffice.

  1. SQLActions[] = {
  2.   "BEGIN INSTALL
  3.     CREATE FUNCTION javatest.hello(varchar)
  4.       RETURNS varchar
  5.       AS 'sandbox.PLJava.hello'
  6.       LANGUAGE java;
  7.   END INSTALL",
  8.   "BEGIN REMOVE
  9.     DROP FUNCTION javatest.hello(varchar);
  10.   END REMOVE"
  11. }
SQLActions[] = {
  "BEGIN INSTALL
     CREATE FUNCTION javatest.hello(varchar)
       RETURNS varchar
       AS 'sandbox.PLJava.hello'
       LANGUAGE java;
   END INSTALL",
  "BEGIN REMOVE
     DROP FUNCTION javatest.hello(varchar);
   END REMOVE"
}

You must tell the deployer about the deployment descriptor in the jar’s MANIFEST.MF file. A sample maven plugin is

  1. <plugin>
  2.    <groupId>org.apache.maven.plugins</groupId>
  3.    <artifactId>maven-jar-plugin</artifactId>
  4.    <version>2.3.1</version>
  5.    <configuration>
  6.       <archive>
  7.          <manifestSections>
  8.             <manifestSection>
  9.                <name>postgresql.ddr</name> <!-- filename -->
  10.                <manifestEntries>
  11.                   <SQLJDeploymentDescriptor>TRUE</SQLJDeploymentDescriptor>
  12.                </manifestEntries>
  13.             </manifestSection>
  14.          </manifestSections>
  15.       </archive>
  16.    </configuration>
  17. </plugin>
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <version>2.3.1</version>
   <configuration>
      <archive>
         <manifestSections>
            <manifestSection>
               <name>postgresql.ddr</name> <!-- filename -->
               <manifestEntries>
                  <SQLJDeploymentDescriptor>TRUE</SQLJDeploymentDescriptor>
               </manifestEntries>
            </manifestSection>
         </manifestSections>
      </archive>
   </configuration>
</plugin>

The database will now know about our methods as they areinstalled and removed.

Internal Queries

One of the ‘big wins’ with stored procedures is that queries are executed on the server itself and are MUCH faster than running them through the programmatic interface. I’ve seen a process that required over 30 minutes via Java knocked down to a fraction of a second by simply moving the queried loop from the client to the server.

The JDBC URL for the internal connection is “jdbc:default:connection”. You cannot use transactions (since you’re within the caller’s transaction) but you can use savepoints as long as you stay within a single call. I don’t know if you can use CallableStatements (other stored procedures yet) – you couldn’t in version 1.2 but the Ubuntu 11.10 package uses version 1.4.2.

Upcoming

Next time I will discuss working with lists and triggers, followed by user-defined types.

More Information

TADA wiki – the author’s blog.

http://pgfoundry.org/projects/pljava/ – Home of PL/Java project.

PL/Java 1.2 User Guide (Note: the Ubuntu package uses release 1.4.2)

java

Using Google Authenticator (TOTP) On Your Site

By thebearinboulderNo Comments

Let’s say you want to use two-factor authentication on your site. (Blog entries to follow…). How do you do it?

Time-based One-Time Passwords (TOTP)

An increasingly popular approach is Time-based One-Time Passwords (TOTP) (RFC6238). This is a straightforward algorithm that only requires an accurate clock and a shared secret.

Accurate times have been a pain in the past – computers did not include particularly good real time clock chips – but any server should now be using NTP. I think the major distributions set it up by default but could be mistaken about that.

Modern cell phones also have the accurate time since they include GPS receivers.

Finally dongles with LCD displays can include accurate clocks, esp. if you’re able to periodically synchronize them to a PC.

Put it together and we can have reasonable confidence that we’ll have matching clocks on the client and server so TOTP becomes a good option.

Jumping straight to the code – this is the reference implementation from the RFC. The RFC also includes test vectors to verify implementations.

  1. /**
  2.  Copyright (c) 2011 IETF Trust and the persons identified as
  3.  authors of the code. All rights reserved.
  4.  
  5.  Redistribution and use in source and binary forms, with or without
  6.  modification, is permitted pursuant to, and subject to the license
  7.  terms contained in, the Simplified BSD License set forth in Section
  8.  4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
  9.  (http://trustee.ietf.org/license-info).
  10.  */
  11. import java.lang.reflect.UndeclaredThrowableException;
  12.  
  13. import java.math.BigInteger;
  14.  
  15. import java.security.GeneralSecurityException;
  16.  
  17. import java.text.DateFormat;
  18. import java.text.SimpleDateFormat;
  19.  
  20. import java.util.Date;
  21. import java.util.TimeZone;
  22.  
  23. import javax.crypto.Mac;
  24. import javax.crypto.spec.SecretKeySpec;
  25.  
  26. /**
  27.  * This is an example implementation of the OATH
  28.  * TOTP algorithm.
  29.  * Visit www.openauthentication.org for more information.
  30.  *
  31.  * @author Johan Rydell, PortWise, Inc.
  32.  */
  33. public class TOTP {
  34.     private static final int[] DIGITS_POWER
  35.     // 0 1  2   3    4     5      6       7        8
  36.          = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };
  37.  
  38.     private TOTP() {
  39.     }
  40.  
  41.     /**
  42.      * This method uses the JCE to provide the crypto algorithm.
  43.      * HMAC computes a Hashed Message Authentication Code with the
  44.      * crypto hash algorithm as a parameter.
  45.      *
  46.      * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
  47.      *                             HmacSHA512)
  48.      * @param keyBytes: the bytes to use for the HMAC key
  49.      * @param text: the message or text to be authenticated
  50.      */
  51.     private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
  52.         try {
  53.             Mac hmac;
  54.             hmac = Mac.getInstance(crypto);
  55.  
  56.             SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
  57.             hmac.init(macKey);
  58.  
  59.             return hmac.doFinal(text);
  60.         } catch (GeneralSecurityException gse) {
  61.             throw new UndeclaredThrowableException(gse);
  62.         }
  63.     }
  64.  
  65.     /**
  66.      * This method converts a HEX string to Byte[]
  67.      *
  68.      * @param hex: the HEX string
  69.      *
  70.      * @return: a byte array
  71.      */
  72.     private static byte[] hexStr2Bytes(String hex) {
  73.         // Adding one byte to get the right conversion
  74.         // Values starting with "0" can be converted
  75.         byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();
  76.  
  77.         // Copy all the REAL bytes, not the "first"
  78.         byte[] ret = new byte[bArray.length - 1];
  79.  
  80.         for (int i = 0; i < ret.length; i++)
  81.             ret[i] = bArray[i + 1];
  82.  
  83.         return ret;
  84.     }
  85.  
  86.     /**
  87.      * This method generates a TOTP value for the given
  88.      * set of parameters.
  89.      *
  90.      * @param key: the shared secret, HEX encoded
  91.      * @param time: a value that reflects a time
  92.      * @param returnDigits: number of digits to return
  93.      *
  94.      * @return: a numeric String in base 10 that includes
  95.      *              {@link truncationDigits} digits
  96.      */
  97.     public static String generateTOTP(String key, String time,
  98.         String returnDigits) {
  99.         return generateTOTP(key, time, returnDigits, "HmacSHA1");
  100.     }
  101.  
  102.     /**
  103.      * This method generates a TOTP value for the given
  104.      * set of parameters.
  105.      *
  106.      * @param key: the shared secret, HEX encoded
  107.      * @param time: a value that reflects a time
  108.      * @param returnDigits: number of digits to return
  109.      *
  110.      * @return: a numeric String in base 10 that includes
  111.      *              {@link truncationDigits} digits
  112.      */
  113.     public static String generateTOTP256(String key, String time,
  114.         String returnDigits) {
  115.         return generateTOTP(key, time, returnDigits, "HmacSHA256");
  116.     }
  117.  
  118.     /**
  119.      * This method generates a TOTP value for the given
  120.      * set of parameters.
  121.      *
  122.      * @param key: the shared secret, HEX encoded
  123.      * @param time: a value that reflects a time
  124.      * @param returnDigits: number of digits to return
  125.      *
  126.      * @return: a numeric String in base 10 that includes
  127.      *              {@link truncationDigits} digits
  128.      */
  129.     public static String generateTOTP512(String key, String time,
  130.         String returnDigits) {
  131.         return generateTOTP(key, time, returnDigits, "HmacSHA512");
  132.     }
  133.  
  134.     /**
  135.      * This method generates a TOTP value for the given
  136.      * set of parameters.
  137.      *
  138.      * @param key: the shared secret, HEX encoded
  139.      * @param time: a value that reflects a time
  140.      * @param returnDigits: number of digits to return
  141.      * @param crypto: the crypto function to use
  142.      *
  143.      * @return: a numeric String in base 10 that includes
  144.      *              {@link truncationDigits} digits
  145.      */
  146.     public static String generateTOTP(String key, String time,
  147.         String returnDigits, String crypto) {
  148.         int codeDigits = Integer.decode(returnDigits).intValue();
  149.         String result = null;
  150.  
  151.         // Using the counter
  152.         // First 8 bytes are for the movingFactor
  153.         // Compliant with base RFC 4226 (HOTP)
  154.         while (time.length() < 16)
  155.             time = "0" + time;
  156.  
  157.         // Get the HEX in a Byte[]
  158.         byte[] msg = hexStr2Bytes(time);
  159.         byte[] k = hexStr2Bytes(key);
  160.         byte[] hash = hmac_sha(crypto, k, msg);
  161.  
  162.         // put selected bytes into result int
  163.         int offset = hash[hash.length - 1] & 0xf;
  164.  
  165.         int binary = ((hash[offset] & 0x7f) << 24) |
  166.             ((hash[offset + 1] & 0xff) << 16) |
  167.             ((hash[offset + 2] & 0xff) << <img src='http://invariantproperties.com/wp-includes/images/smilies/icon_cool.gif' alt='8)' class='wp-smiley' /> | (hash[offset + 3] & 0xff);
  168.  
  169.         int otp = binary % DIGITS_POWER[codeDigits];
  170.  
  171.         result = Integer.toString(otp);
  172.  
  173.         while (result.length() < codeDigits) {
  174.             result = "0" + result;
  175.         }
  176.  
  177.         return result;
  178.     }
  179.  
  180.     public static void main(String[] args) {
  181.         // Seed for HMAC-SHA1 - 20 bytes
  182.         String seed = "3132333435363738393031323334353637383930";
  183.  
  184.         // Seed for HMAC-SHA256 - 32 bytes
  185.         String seed32 = "3132333435363738393031323334353637383930" +
  186.             "313233343536373839303132";
  187.  
  188.         // Seed for HMAC-SHA512 - 64 bytes
  189.         String seed64 = "3132333435363738393031323334353637383930" +
  190.             "3132333435363738393031323334353637383930" +
  191.             "3132333435363738393031323334353637383930" + "31323334";
  192.         long T0 = 0;
  193.         long X = 30;
  194.         long[] testTime = {
  195.                 59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L,
  196.                 20000000000L
  197.             };
  198.  
  199.         String steps = "0";
  200.         DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  201.         df.setTimeZone(TimeZone.getTimeZone("UTC"));
  202.  
  203.         try {
  204.             System.out.println("+---------------+-----------------------+" +
  205.                 "------------------+--------+--------+");
  206.             System.out.println("|  Time(sec)    |   Time (UTC format)   " +
  207.                 "| Value of T(Hex)  |  TOTP  | Mode   |");
  208.             System.out.println("+---------------+-----------------------+" +
  209.                 "------------------+--------+--------+");
  210.  
  211.             for (int i = 0; i < testTime.length; i++) {
  212.                 long T = (testTime[i] - T0) / X;
  213.                 steps = Long.toHexString(T).toUpperCase();
  214.  
  215.                 while (steps.length() < 16)
  216.                     steps = "0" + steps;
  217.  
  218.                 String fmtTime = String.format("%1$-11s", testTime[i]);
  219.                 String utcTime = df.format(new Date(testTime[i] * 1000));
  220.                 System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
  221.                     steps + " |");
  222.                 System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1") +
  223.                     "| SHA1   |");
  224.                 System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
  225.                     steps + " |");
  226.                 System.out.println(generateTOTP(seed32, steps, "8", "HmacSHA256") +
  227.                     "| SHA256 |");
  228.                 System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
  229.                     steps + " |");
  230.                 System.out.println(generateTOTP(seed64, steps, "8", "HmacSHA512") +
  231.                     "| SHA512 |");
  232.  
  233.                 System.out.println("+---------------+-----------------------+" +
  234.                     "------------------+--------+--------+");
  235.             }
  236.         } catch (final Exception e) {
  237.             System.out.println("Error : " + e);
  238.         }
  239.     }
  240. }
/**
 Copyright (c) 2011 IETF Trust and the persons identified as
 authors of the code. All rights reserved.

 Redistribution and use in source and binary forms, with or without
 modification, is permitted pursuant to, and subject to the license
 terms contained in, the Simplified BSD License set forth in Section
 4.c of the IETF Trust's Legal Provisions Relating to IETF Documents
 (http://trustee.ietf.org/license-info).
 */
import java.lang.reflect.UndeclaredThrowableException;

import java.math.BigInteger;

import java.security.GeneralSecurityException;

import java.text.DateFormat;
import java.text.SimpleDateFormat;

import java.util.Date;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * This is an example implementation of the OATH
 * TOTP algorithm.
 * Visit www.openauthentication.org for more information.
 *
 * @author Johan Rydell, PortWise, Inc.
 */
public class TOTP {
    private static final int[] DIGITS_POWER
    // 0 1  2   3    4     5      6       7        8
         = { 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000 };

    private TOTP() {
    }

    /**
     * This method uses the JCE to provide the crypto algorithm.
     * HMAC computes a Hashed Message Authentication Code with the
     * crypto hash algorithm as a parameter.
     *
     * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256,
     *                             HmacSHA512)
     * @param keyBytes: the bytes to use for the HMAC key
     * @param text: the message or text to be authenticated
     */
    private static byte[] hmac_sha(String crypto, byte[] keyBytes, byte[] text) {
        try {
            Mac hmac;
            hmac = Mac.getInstance(crypto);

            SecretKeySpec macKey = new SecretKeySpec(keyBytes, "RAW");
            hmac.init(macKey);

            return hmac.doFinal(text);
        } catch (GeneralSecurityException gse) {
            throw new UndeclaredThrowableException(gse);
        }
    }

    /**
     * This method converts a HEX string to Byte[]
     *
     * @param hex: the HEX string
     *
     * @return: a byte array
     */
    private static byte[] hexStr2Bytes(String hex) {
        // Adding one byte to get the right conversion
        // Values starting with "0" can be converted
        byte[] bArray = new BigInteger("10" + hex, 16).toByteArray();

        // Copy all the REAL bytes, not the "first"
        byte[] ret = new byte[bArray.length - 1];

        for (int i = 0; i < ret.length; i++)
            ret[i] = bArray[i + 1];

        return ret;
    }

    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *              {@link truncationDigits} digits
     */
    public static String generateTOTP(String key, String time,
        String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA1");
    }

    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *              {@link truncationDigits} digits
     */
    public static String generateTOTP256(String key, String time,
        String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA256");
    }

    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     *
     * @return: a numeric String in base 10 that includes
     *              {@link truncationDigits} digits
     */
    public static String generateTOTP512(String key, String time,
        String returnDigits) {
        return generateTOTP(key, time, returnDigits, "HmacSHA512");
    }

    /**
     * This method generates a TOTP value for the given
     * set of parameters.
     *
     * @param key: the shared secret, HEX encoded
     * @param time: a value that reflects a time
     * @param returnDigits: number of digits to return
     * @param crypto: the crypto function to use
     *
     * @return: a numeric String in base 10 that includes
     *              {@link truncationDigits} digits
     */
    public static String generateTOTP(String key, String time,
        String returnDigits, String crypto) {
        int codeDigits = Integer.decode(returnDigits).intValue();
        String result = null;

        // Using the counter
        // First 8 bytes are for the movingFactor
        // Compliant with base RFC 4226 (HOTP)
        while (time.length() < 16)
            time = "0" + time;

        // Get the HEX in a Byte[]
        byte[] msg = hexStr2Bytes(time);
        byte[] k = hexStr2Bytes(key);
        byte[] hash = hmac_sha(crypto, k, msg);

        // put selected bytes into result int
        int offset = hash[hash.length - 1] & 0xf;

        int binary = ((hash[offset] & 0x7f) << 24) |
            ((hash[offset + 1] & 0xff) << 16) |
            ((hash[offset + 2] & 0xff) << <img src='http://invariantproperties.com/wp-includes/images/smilies/icon_cool.gif' alt='8)' class='wp-smiley' /> | (hash[offset + 3] & 0xff);

        int otp = binary % DIGITS_POWER[codeDigits];

        result = Integer.toString(otp);

        while (result.length() < codeDigits) {
            result = "0" + result;
        }

        return result;
    }

    public static void main(String[] args) {
        // Seed for HMAC-SHA1 - 20 bytes
        String seed = "3132333435363738393031323334353637383930";

        // Seed for HMAC-SHA256 - 32 bytes
        String seed32 = "3132333435363738393031323334353637383930" +
            "313233343536373839303132";

        // Seed for HMAC-SHA512 - 64 bytes
        String seed64 = "3132333435363738393031323334353637383930" +
            "3132333435363738393031323334353637383930" +
            "3132333435363738393031323334353637383930" + "31323334";
        long T0 = 0;
        long X = 30;
        long[] testTime = {
                59L, 1111111109L, 1111111111L, 1234567890L, 2000000000L,
                20000000000L
            };

        String steps = "0";
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        df.setTimeZone(TimeZone.getTimeZone("UTC"));

        try {
            System.out.println("+---------------+-----------------------+" +
                "------------------+--------+--------+");
            System.out.println("|  Time(sec)    |   Time (UTC format)   " +
                "| Value of T(Hex)  |  TOTP  | Mode   |");
            System.out.println("+---------------+-----------------------+" +
                "------------------+--------+--------+");

            for (int i = 0; i < testTime.length; i++) {
                long T = (testTime[i] - T0) / X;
                steps = Long.toHexString(T).toUpperCase();

                while (steps.length() < 16)
                    steps = "0" + steps;

                String fmtTime = String.format("%1$-11s", testTime[i]);
                String utcTime = df.format(new Date(testTime[i] * 1000));
                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
                    steps + " |");
                System.out.println(generateTOTP(seed, steps, "8", "HmacSHA1") +
                    "| SHA1   |");
                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
                    steps + " |");
                System.out.println(generateTOTP(seed32, steps, "8", "HmacSHA256") +
                    "| SHA256 |");
                System.out.print("|  " + fmtTime + "  |  " + utcTime + "  | " +
                    steps + " |");
                System.out.println(generateTOTP(seed64, steps, "8", "HmacSHA512") +
                    "| SHA512 |");

                System.out.println("+---------------+-----------------------+" +
                    "------------------+--------+--------+");
            }
        } catch (final Exception e) {
            System.out.println("Error : " + e);
        }
    }
}

Google Authenticator

Enter Google. Or more precisely, Google Accounts. This is a popular hosting platform for small businesses, non-profits and groups. Even individuals with vanity domains.

Many of these users require better security than you get with just a password. Some users REQUIRE better security due to regulatory or contractual obligations.

Google saw the problem and came up with a solution: Google Authenticator. It is an open source implementation of the TOTP algorithm that has been implemented on smart phones and as a Linux PAM module. Not everyone has a smart phone but enough do for this to be a good solution. Hardware dongles are also available if you prefer them.

(IMPORTANT: the security of the Linux PAM module is debatable since it includes the user’s secret key, unencrypted, in the user’s home directory.)

The code to generate the code produced by Google Authenticator implementations is:

  1. byte[] key = new byte[8];  // = TOPT.exStr2Bytes(keyInHex);
  2. long counter = System.currentTimeMillis() / 30000L;
  3.  
  4. String code = TOTP.generateTOTP(key, counter, "6", "HmacSHA1");
byte[] key = new byte[8];  // = TOPT.exStr2Bytes(keyInHex);
long counter = System.currentTimeMillis() / 30000L;

String code = TOTP.generateTOTP(key, counter, "6", "HmacSHA1");

(Sidenote: it goes without saying that the googlecode project includes similar code. I’m using the RFC reference implementation since it’s much more flexible – I might want to use different parameters in other situations.)

Server-side Implementation

The server-side implementation is fairly straightforward.

Registration

  • Create a random 8-byte secret key. Be sure to use a cryptographically strong random number generator (SecureRandom), not the standard random number generator.
  • Save the key in the database. ENCRYPT IT.
  • Create a unique label, e.g., the username @ the site’s domain name. You don’t want to use anything that can change, e.g., the user’s email address, or something that might be used at other sites, e.g., nothing but his username. Remember that this is what is used to remind the user to use this key – you don’t want to use random strings.
  • Provide the user with a QR code that he can scan using his smart phone. You can also provide the secret key in a string for he user to enter manually.

There is a standard URI for providing secret keys.

The URI for a TOTP key is otpauth://totp/LABEL?secret=SECRET where LABEL is the unique identifier you created above and SECRET is the base32-encoded shared secret.

Base32 uses the case-insensitive letters A-Z and the digits 2-7 to encode a value and corrects for several common errors, e.g., using ’0′ instead of ‘O’. An encoder is available in the Apache commons-codec project.

You can get QR image for this URI at google.com, e.g., for otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP it is

<img src=”https://www.google.com/chart?chs=200×200&chld=M|0&cht=qr&chl=otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP”/>

With the QR code the user can simply point his smartphone at his monitor to load his key.

Verification

  • The user is prompted for his username, password, and TOTP code
  • When the login form is received three codes are generated – for the current time, 30 seconds ago, and 30 seconds from now. This gives you a bit of buffer to allow for unsynchronized clocks or the time required by the user to enter the data and submit the form.
  • Check the codes and respond accordingly.

Lost or Compromised Keys

People lose the phones. Local policy may require access credentials to be changed periodically. You have to be prepared.

  • The user authenticates himself using the current TOTP code (if it’s a a periodic change) or via some other mechanism. (Do not ask the common easy to guess questions!)
  • A new secret key is created and provided to the user as described above.
  • The old secret key is either deleted (low security) or retained to capture attempted uses in the future (high security).

As always you never provide current credentials to the user.

Crash Codes

Sometimes people do not have access to their phone (e.g., they’re in a secured environment) or are otherwise unable to use the TOTP code. We must provide a fallback mechanism.

Fortunately this is very easy – provide the user with the eight digit code for the first few values, say counter = 0 to counter = 5. Strictly speaking these are now Hash-based One-Time Passwords (HOTP).

The user should treat these codes in the same way as passwords.

The authentication process now checks the current time for 6 digit codes, or the first few counters for 8 digit codes.

Oracles

I mentioned that the secret key should be encrypted but we can make this design much more robust by using an oracle. These are stored procedures that encapsulate all of the logic and the application is provided the absolute minimum amount of information.

The stored procedure signatures are

  1. --
  2. -- Generate a random key, associate it with the user, and return
  3. -- the corresponding otpauth URI.
  4. --
  5. -- This stored procedure is also responsible for moving any
  6. -- existing key to an audit table if future attempts to log
  7. -- in with the code should be recorded.
  8. --
  9. CREATE FUNCTION generateTotpUri(username varchar, label varchar) RETURNS varchar AS $$
  10. -- body...
  11. $$
  12.  
  13. --
  14. -- Authenticate the user with the specified password and TOTP code.
  15. -- This stored procedure should accept either 6 digit time-based TOTP
  16. -- codes or 8 digit HOTP codes.
  17. --
  18. -- This stored procedure should only return 1 (success) or 0 (failure).
  19. --
  20. -- This stored procedure is also responsible for recording
  21. -- any attempts to log in with disabled secret keys.
  22. --
  23. CREATE FUNCTION authenticateUser(username varchar, password varchar, totp varchar) RETURNS intege AS $$
  24. -- body...
  25. $$
--
-- Generate a random key, associate it with the user, and return
-- the corresponding otpauth URI.
--
-- This stored procedure is also responsible for moving any
-- existing key to an audit table if future attempts to log
-- in with the code should be recorded.
--
CREATE FUNCTION generateTotpUri(username varchar, label varchar) RETURNS varchar AS $$
-- body...
$$

--
-- Authenticate the user with the specified password and TOTP code.
-- This stored procedure should accept either 6 digit time-based TOTP
-- codes or 8 digit HOTP codes.
--
-- This stored procedure should only return 1 (success) or 0 (failure).
--
-- This stored procedure is also responsible for recording
-- any attempts to log in with disabled secret keys.
--
CREATE FUNCTION authenticateUser(username varchar, password varchar, totp varchar) RETURNS intege AS $$
-- body...
$$

Many databases support SHA1 HMAC computations, e.g., in the pgcrypto package for PostgreSQL. The trick to remember is that the last nybble of the hash is used as the offset into the hash before converting the hash into an integer – that’s not a common approach yet.

Implementation is left as an exercise for the reader. (Hint: you can always be lazy and ask the database crypto provider to add this function!)

For More Information

http://code.google.com/p/google-authenticator/

Google Authenticator for multi-factor authentication

RFC 6238 (TOTP)

RFC 4226 (HOTP)

PostgreSQL pgcrypto package – note: this is for release 8.3. I don’t know if there have been more recent updates.

Late update: another article went up at Java Code Geeks while I was working on mine: Google Authenticator: Using It With Your Own Java Authentication Server

java, security

Database and Webapp Security, part 5: User Authentication

By thebearinboulderNo Comments

User Authentication and Authorization Information

User authentication (authn) is how we know that a user is who he claims to be. At a minimum it’s a username and password but it could include much more if two-factor authentication is used.

User authorization (authz) is what we allow the user to do.

These are very different questions and should be treated as such. Some architectures for this, e.g., if a site uses siteminder or a similar tool then it doesn’t have access to authn information at all – it can only add authz.

What is user authn/authz information? It is

  • username and/or email
  • password
  • single sign-on (SSO) identifications
  • security tokens (for two-factor authentication)
  • security images/phrases (used to prove your site is legitimate to the user)
  • groups and roles
What is not user authn/authz information?
  • contact information
  • content subscriptions
  • or anything else that’s not required to authenticate or authorize the user.

Wrong Approach

Put everything – user authn/authz, static content and dynamic content – into a single database schema.

It’s quick.

It’s easy.

It’s the default behavior for auto-generation tools.

And it’s very, very wrong since anyone who cracks your webapp has also cracked your user authn/authz data. At best you’ll have a denial-of-service. At worst they can pretend to be other users, can add their own highly-priviledged account, etc.

Separate Schemas and Connection Pools

The quickest solution is to create a separate schema for the user authn/authz data and use a dedicated data source (or Hibernate session) when accessing this data. This schema should be unreadable from the standard data source (or Hibernate session). This gives you a good firewall from the world but isn’t perfect.

A seemingly more robust solution is to use a separate database, not just a separate schema, for the user authn/auhtz data. This would seem to protect you from misconfigured rights that would allow the dynamic content data source to access the user data source.

Sadly in some RDMBS there’s not a clear distinction between schemas and databases and a connection to one “database” can still access another “database” if the necessary rights are granted. You can’t be sure unless you have a separate database instance for user authn/authz and dynamic content. This may not be an undue burden if your architecture has a server dedicated for user authn/authz. This is not unreasonable with virtual servers or a cloud design.

Container-Based Authentication

A better solution is container-based authentication. Pull user authn/authz entirely out of the webapp – by the time your webapp gets the request the HttpServletRequest already has all necessary information populated. Your webapp has no access to the container’s authentication information. (Modulo the notes above – you don’t gain anything if the container looks into the same schema as your dynamic content.)

A variant of this is authentication filters put in front of the webapp, e.g., those from Spring Security. It’s a different mechanism but serves the same purpose of keeping a very sharp distinction between user data and dynamic content.

The Glitch – Adding and Updating Users

There’s one big glitch here – how do you add or update user information if your webapp can’t access the user authn/authz tables?

The first approach is to create a separate webapp that handles this. Your main webapp can transparently redirect to the second webapp as necessary. The upside is that you can have a consistent look and feel, the downside is that you’re exposing user authn/authz information to the weeb again.

The second approach is to create a separate REST service that handles this. Your webapp can provide the user interface but call the REST service instead of the standard business layer. The REST service can be within your firewall.

The third approach is to defer this entirely to the container. This ensures maximum separation but makes it difficult to have a consistent look and feel.

java, linux, security

Revisiting Defending from XEE Attacks With Security Managers

By thebearinboulderNo Comments

A little over a year ago I wrote about using security managers and used XEE attacks (that is, using XML External Entities (XEE) in XML for denial of service or information disclosure attacks) in passing.

Using a SecurityManager and Identifying Requirements.

Since then I’ve had a ‘duh’ moment and realized a simple solution to this problem. I am currently working on a longer piece but XEE attacks are serious enough to warrant a quick update to the earlier piece.

In a nutshell there’s no need to explicitly list all of the required permissions – we can simply train our SecurityManager. The process is straightforward:

  • write a logging SecurityManager as described earlier[1]
  • process a few sample documents that exercise all of the requirements
  • flip a flag
  • the SecurityManager now checks all future requests against the permissions already seen. If there’s a match it’s permitted. If not it’s denied.

A human should still review the identified permissions before putting this into production – you want to ensure it’s the most restrictive permissions. E.g., you should only grant permission to read a specific file, not an arbitrary file.

That’s the problem holding up the broader paper. XML processing is a very tightly constrained problem and we can identify precisely what’s required. We can’t say that about many other problems so we would want wildcard matching of pre-specified permissions and that’s a lot more complex.

[1] I would make one change today – I would call the existing SecurityManager, if it exists, instead of blindly accepting all requests.

java, security

Database and Webapp Security, part 4: Schema Ownership

By thebearinboulderNo Comments

What are DDL, DML, DCL and TCL?

SQL contains four distinct types of statements.

Data Definition Language

Data Definition Language (DDL) statements define the database structure. Think of this as the landlord that builds the warehouse but turns over the keys to the renter.

Statements:

  • create – create tables, views, indexes, etc.
  • alter – alter tables, views, indexes, columns, etc.
  • drop – delete tables, views, indexes, etc.
  • truncate – remove all of the records from a table
  • comment – add comments to tables, columns, views, etc.
  • rename – rename a table, view, etc.

Data Manipulation Language

Data Manipulation Language (DML) statements manage the data within the structure created by the DDL. Think of this as the tenant of the warehouse – it can use the warehouse but can’t knock down walls.

Statements:

  • select – retrieve data
  • insert – insert new data into a table
  • update – update existing data within a table
  • delete – delete data from a table
  • call – call a PL/SQL or other stored procedure
  • explain plan – explain how a querywill be executed
  • lock table – lock a table to limit concurrency

Data Control Language

Data Control Language (CDL) statements control access rights to the data and schema. Think of these as locks on the doors, permission to move walls within the warehouse, etc.

Statements:

  • grant – give the user additional privileges
  • revoke – remove user privileges

Transaction Control Language

Transaction Control Language (TCL) statement are used to control transactions.

Statements:

  • commit – save completed work
  • rollback – undo completed work
  • savepoint – mark a point that we can rollback to later without necessarily rolling back the entire transaction
  • set transaction – set transaction options

Use Different Database User for Schema And Data Ownership

The schema should be owned by one database user, e.g., app_owner and the data should be owned by a different database user, e.g., app_user.

The owner should:

  • have the ability to run DDL and DCL statements
  • arguably not have the ability to run DML statements
  • never be accessed via the webapp

The user should

  • have the ability to run DML and TCL statements
  • not have the ability to run DDL or DCL statements
  • be accessible via the webapp

Cost/Benefit Analysis

There is a very favorable cost/benefit ratio for separating the ownership of schema and database. There is a slightly higher cost when creating and maintaining the database but it essentially eliminates the ability of a web intruder to destroy the database schema itself. The data, on the other hand, can still be nuked.

java, linux, security

Database and Webapp Security, part 3: SQL Injection in Stored Procedures

By thebearinboulderNo Comments

What are stored procedures and CallableStatements?

Stored procedures are bits of code kept in the database. The most common form is a SQL-like scripting language but additional languages are supported – PERL, tcl, ruby, java, etc.

It is important to remember that stored procedures are used in database triggers – you should be aware of them even if you all of your work with hibernate.

Wrong Approach

The wrong approach is to create a dynamic SQL query without sanitization.

  1. DELIMITER $$
  2. DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
  3. CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
  4.                                  IN password VARCHAR(20),
  5.                                  OUT success INT)
  6. BEGIN
  7.   SET @query = CONCAT('SELECT COUNT(credentials.username) INTO @succ
  8.     FROM credentials
  9.     WHERE credentials.username = \'', username,
  10.         '\' AND credentials.password = \'', password, '\'');
  11.    PREPARE stmt FROM @query;
  12.    EXECUTE stmt;
  13.    SELECT @succ;
  14.    SET success = @succ;
  15. END;
  16. $$
  17. DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
                                 IN password VARCHAR(20),
                                 OUT success INT)
BEGIN
  SET @query = CONCAT('SELECT COUNT(credentials.username) INTO @succ
     FROM credentials
     WHERE credentials.username = \'', username,
        '\' AND credentials.password = \'', password, '\'');
   PREPARE stmt FROM @query;
   EXECUTE stmt;
   SELECT @succ;
   SET success = @succ;
END;
$$
DELIMITER ;

(Note: this code fragment is an example comes from the reference below.)

This stored procedure has no benefits over the “wrong answer” in the part 2 with the exception of very modest encapsulation..

Sidenote: This is an example of an oracle. It returns the minimum amount of information about user authentication – a “thumbs up” or “thumbs down”. There’s no information leak in this implementation since the caller already knows the username and password but a more robust implementation could also verify that the user account has not been disabled, etc.

Stored Procedures and Parameterization

The first safe approach is executing the SQL directly instead of creating dynamic SQL.

The second safe approach is parameterization within the stored procedure. This is directly equivalent to Java prepared statements and placeholders.

  1. DELIMITER $$
  2. DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
  3. CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
  4.                                  IN password VARCHAR(20),
  5.                                  OUT success INT)
  6. BEGIN
  7.   SET @query = 'SELECT COUNT(credentials.username) INTO @succ
  8.     FROM credentials
  9.     WHERE credentials.username = ? AND credentials.password = ?';
  10.    PREPARE stmt FROM @query;
  11.    SET @usernm = username;
  12.    SET @pass = password;
  13.    EXECUTE stmt USING @usernm, @pass;
  14.    SELECT @succ;
  15.    SET success = @succ;
  16. END;
  17. $$
  18. DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
                                 IN password VARCHAR(20),
                                 OUT success INT)
BEGIN
  SET @query = 'SELECT COUNT(credentials.username) INTO @succ
     FROM credentials
     WHERE credentials.username = ? AND credentials.password = ?';
   PREPARE stmt FROM @query;
   SET @usernm = username;
   SET @pass = password;
   EXECUTE stmt USING @usernm, @pass;
   SELECT @succ;
   SET success = @succ;
END;
$$
DELIMITER ;

PLPSQL Sanitization

There is another alternative if you’re willing to be tied to a specific database vendor. In practice this usually isn’t an issue – hibernate gives you some database transparency but stored procedures will always be tied closely to the database.

In plpsql (PostgreSQL) there are two commands that can be used for sanitizing input: quote_ident and quote_literal. There are undoubtably similar commands in other stored procedure languages.

Updating the wrong answer above we have:

  1. DELIMITER $$
  2. DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
  3. CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
  4.                                  IN password VARCHAR(20),
  5.                                  OUT success INT)
  6. BEGIN
  7.   SET @query = CONCAT('SELECT COUNT(credentials.username) INTO @succ
  8.     FROM credentials
  9.     WHERE credentials.username = ', quote_literal(username),
  10.         'AND credentials.password = ', quote_literal(password));
  11.    PREPARE stmt FROM @query;
  12.    EXECUTE stmt;
  13.    SELECT @succ;
  14.    SET success = @succ;
  15. END;
  16. $$
  17. DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(20),
                                 IN password VARCHAR(20),
                                 OUT success INT)
BEGIN
  SET @query = CONCAT('SELECT COUNT(credentials.username) INTO @succ
     FROM credentials
     WHERE credentials.username = ', quote_literal(username),
        'AND credentials.password = ', quote_literal(password));
   PREPARE stmt FROM @query;
   EXECUTE stmt;
   SELECT @succ;
   SET success = @succ;
END;
$$
DELIMITER ;

Direct SQL

The final safe approach is to use direct SQL calls with minimum parameter size. This is mentioned on the CERT website but I would hesitate to use it since it would be so easy to introduce unsafe code by accident.

  1. DELIMITER $$
  2. DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
  3. CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(8),
  4.                                  IN password VARCHAR(20),
  5.                                  OUT success INT)
  6. BEGIN
  7.   SELECT COUNT(credentials.username) INTO success
  8.      FROM credentials
  9.      WHERE credentials.username = username AND credentials.password = password;
  10. END;
  11. $$
  12. DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS SP_AUTHENTICATE$$
CREATE PROCEDURE SP_AUTHENTICATE(IN username VARCHAR(8),
                                 IN password VARCHAR(20),
                                 OUT success INT)
BEGIN
  SELECT COUNT(credentials.username) INTO success
     FROM credentials
     WHERE credentials.username = username AND credentials.password = password;
END;
$$
DELIMITER ;

Cost/Benefit Analysis

Stored procedures are harder to exploit than naked SQL queries but this often gives people a false sense of security. This should be considered mandatory for sensitive information (user authentication, audit logging) and highly recommended in all other cases.

References

https://www.securecoding.cert.org/confluence/pages/viewpage.action?pageId=70288108

java, linux, security
Blue Taste Theme created by Jabox