Creating-a-scalar-udt-in-java
Creating a scalar (or, base) user-defined type
This text assumes that you have some familiarity with how scalar types are created and added to the PostgreSQL type system. For more info on that topic, please read this chapter in the PostgreSQL docs.
Creating new scalar type using Java functions is very similar to how they are
created using C functions from an SQL perspective but of course very different
when looking at the actual implementation. Java stipulates that the mapping
between a Java class and a corresponding SQL type should be done using the
interfaces java.sql.SQLData
, java.sql.SQLInput
, and
java.sql.SQLOutput
and that is what PL/Java is using. In addition, the
PostgreSQL type system stipulates that each type must have a textual
representation.
Let us create a type called javatest.complex
(similar to the complex
type used in the PostgreSQL documentation). The name of the corresponding
Java class will be org.postgresql.pljava.example.ComplexScalar
.
The Java code for the scalar type
Prerequisites for the Java implementation
The java class for a scalar UDT must implement the java.sql.SQLData
interface. In addition, it must also implement a method
static T parse(String stringRepresentation, String typeName)
where T
will
be the name of the class–that is, parse
will create and return an instance
of the class–and the java.lang.String toString()
method.
The toString()
method must return something
that the parse()
method can parse.
package org.postgresql.pljava.example;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.sql.SQLData;
import java.sql.SQLException;
import java.sql.SQLInput;
import java.sql.SQLOutput;
import java.util.logging.Logger;
import org.postgresql.pljava.annotation.Function;
import org.postgresql.pljava.annotation.SQLType;
import org.postgresql.pljava.annotation.BaseUDT;
import static org.postgresql.pljava.annotation.Function.Effects.IMMUTABLE;
import static
org.postgresql.pljava.annotation.Function.OnNullInput.RETURNS_NULL;
@BaseUDT(schema="javatest", name="complex",
internalLength=16, alignment=BaseUDT.Alignment.DOUBLE)
public class ComplexScalar implements SQLData
{
private double m_x;
private double m_y;
private String m_typeName;
@Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
public static ComplexScalar parse(String input, String typeName)
throws SQLException
{
try
{
StreamTokenizer tz = new StreamTokenizer(new StringReader(input));
if(tz.nextToken() == '('
&& tz.nextToken() == StreamTokenizer.TT_NUMBER)
{
double x = tz.nval;
if(tz.nextToken() == ','
&& tz.nextToken() == StreamTokenizer.TT_NUMBER)
{
double y = tz.nval;
if(tz.nextToken() == ')')
{
return new ComplexScalar(x, y, typeName);
}
}
}
throw new SQLException("Unable to parse complex from string \""
+ input + '"');
}
catch(IOException e)
{
throw new SQLException(e.getMessage());
}
}
public ComplexScalar()
{
}
public ComplexScalar(double x, double y, String typeName)
{
m_x = x;
m_y = y;
m_typeName = typeName;
}
@Override
public String getSQLTypeName()
{
return m_typeName;
}
@Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
@Override
public void readSQL(SQLInput stream, String typeName) throws SQLException
{
m_x = stream.readDouble();
m_y = stream.readDouble();
m_typeName = typeName;
}
@Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
@Override
public void writeSQL(SQLOutput stream) throws SQLException
{
stream.writeDouble(m_x);
stream.writeDouble(m_y);
}
@Function(effects=IMMUTABLE, onNullInput=RETURNS_NULL)
@Override
public String toString()
{
s_logger.info(m_typeName + " toString");
StringBuffer sb = new StringBuffer();
sb.append('(');
sb.append(m_x);
sb.append(',');
sb.append(m_y);
sb.append(')');
return sb.toString();
}
/* Meaningful code that actually does something with this type was
* intentionally left out.
*/
}
The class itself is annotated with @BaseUDT
, giving its SQL schema and name,
and the length and alignment needed for its internal, stored form.
Because the compiler knows the class is a BaseUDT
, it already expects the
parse
, toString
, readSQL
, and writeSQL
methods to be present, and
will generate the correct SQL to declare them as functions to PostgreSQL.
The @Function
annotations are only there to declare the immutability and
on-null-input behavior for those methods, because those values are not the
defaults when declaring a function.