/* * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, * and the EPL 1.0 (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.test.jdbc; import org.h2.api.CustomDataTypesHandler; import org.h2.api.ErrorCode; import org.h2.message.DbException; import org.h2.store.DataHandler; import org.h2.test.TestBase; import org.h2.util.JdbcUtils; import org.h2.util.StringUtils; import org.h2.value.CompareMode; import org.h2.value.DataType; import org.h2.value.Value; import org.h2.value.ValueBytes; import org.h2.value.ValueDouble; import org.h2.value.ValueJavaObject; import org.h2.value.ValueString; import java.io.Serializable; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.text.DecimalFormat; import java.util.Locale; /** * Tests {@link CustomDataTypesHandler}. */ public class TestCustomDataTypesHandler extends TestBase { /** * The database name. */ public final static String DB_NAME = "customDataTypes"; /** * The system property name. */ public final static String HANDLER_NAME_PROPERTY = "h2.customDataTypesHandler"; /** * Run just this test. * * @param a ignored */ public static void main(String... a) throws Exception { System.setProperty(HANDLER_NAME_PROPERTY, TestOnlyCustomDataTypesHandler.class.getName()); TestBase test = createCaller().init(); test.config.traceTest = true; test.config.memory = true; test.config.networked = true; test.config.beforeTest(); test.test(); test.config.afterTest(); System.clearProperty(HANDLER_NAME_PROPERTY); } @Override public void test() throws Exception { try { JdbcUtils.customDataTypesHandler = new TestOnlyCustomDataTypesHandler(); deleteDb(DB_NAME); Connection conn = getConnection(DB_NAME); Statement stat = conn.createStatement(); //Test cast ResultSet rs = stat.executeQuery("select CAST('1-1i' AS complex) + '1+1i' "); rs.next(); assertTrue(rs.getObject(1).equals(new ComplexNumber(2, 0))); //Test create table stat.execute("create table t(id int, val complex)"); rs = conn.getMetaData().getColumns(null, null, "T", "VAL"); rs.next(); assertEquals(rs.getString("TYPE_NAME"), "complex"); assertEquals(rs.getInt("DATA_TYPE"), Types.JAVA_OBJECT); rs = stat.executeQuery("select val from t"); assertEquals(ComplexNumber.class.getName(), rs.getMetaData().getColumnClassName(1)); //Test insert PreparedStatement stmt = conn.prepareStatement( "insert into t(id, val) values (0, '1.0+1.0i'), (1, ?), (2, ?), (3, ?)"); stmt.setObject(1, new ComplexNumber(1, -1)); stmt.setObject(2, "5.0+2.0i"); stmt.setObject(3, 100.1); stmt.executeUpdate(); //Test selects ComplexNumber[] expected = new ComplexNumber[4]; expected[0] = new ComplexNumber(1, 1); expected[1] = new ComplexNumber(1, -1); expected[2] = new ComplexNumber(5, 2); expected[3] = new ComplexNumber(100.1, 0); for (int id = 0; id < expected.length; ++id) { PreparedStatement prepStat =conn.prepareStatement( "select val from t where id = ?"); prepStat.setInt(1, id); rs = prepStat.executeQuery(); assertTrue(rs.next()); assertTrue(rs.getObject(1).equals(expected[id])); } for (int id = 0; id < expected.length; ++id) { PreparedStatement prepStat = conn.prepareStatement( "select id from t where val = ?"); prepStat.setObject(1, expected[id]); rs = prepStat.executeQuery(); assertTrue(rs.next()); assertEquals(rs.getInt(1), id); } // Repeat selects with index stat.execute("create index val_idx on t(val)"); for (int id = 0; id < expected.length; ++id) { PreparedStatement prepStat = conn.prepareStatement( "select id from t where val = ?"); prepStat.setObject(1, expected[id]); rs = prepStat.executeQuery(); assertTrue(rs.next()); assertEquals(rs.getInt(1), id); } // sum function rs = stat.executeQuery("select sum(val) from t"); rs.next(); assertTrue(rs.getObject(1).equals(new ComplexNumber(107.1, 2))); // user function stat.execute("create alias complex_mod for \"" + getClass().getName() + ".complexMod\""); rs = stat.executeQuery("select complex_mod(val) from t where id=2"); rs.next(); assertEquals(complexMod(expected[2]), rs.getDouble(1)); conn.close(); deleteDb(DB_NAME); } finally { JdbcUtils.customDataTypesHandler = null; } } /** * The modulus function. * * @param val complex number * @return result */ public static double complexMod(ComplexNumber val) { return val.mod(); } /** * The custom data types handler to use for this test. */ public static class TestOnlyCustomDataTypesHandler implements CustomDataTypesHandler { /** Type name for complex number */ public final static String COMPLEX_DATA_TYPE_NAME = "complex"; /** Type id for complex number */ public final static int COMPLEX_DATA_TYPE_ID = 1000; /** Order for complex number data type */ public final static int COMPLEX_DATA_TYPE_ORDER = 100_000; /** Cached DataType instance for complex number */ public final DataType complexDataType; /** */ public TestOnlyCustomDataTypesHandler() { complexDataType = createComplex(); } @Override public DataType getDataTypeByName(String name) { if (name.toLowerCase(Locale.ENGLISH).equals(COMPLEX_DATA_TYPE_NAME)) { return complexDataType; } return null; } @Override public DataType getDataTypeById(int type) { if (type == COMPLEX_DATA_TYPE_ID) { return complexDataType; } return null; } @Override public String getDataTypeClassName(int type) { if (type == COMPLEX_DATA_TYPE_ID) { return ComplexNumber.class.getName(); } throw DbException.get( ErrorCode.UNKNOWN_DATA_TYPE_1, "type:" + type); } @Override public int getTypeIdFromClass(Class<?> cls) { if (cls == ComplexNumber.class) { return COMPLEX_DATA_TYPE_ID; } return Value.JAVA_OBJECT; } @Override public Value convert(Value source, int targetType) { if (source.getType() == targetType) { return source; } if (targetType == COMPLEX_DATA_TYPE_ID) { switch (source.getType()) { case Value.JAVA_OBJECT: { assert source instanceof ValueJavaObject; return ValueComplex.get((ComplexNumber) JdbcUtils.deserialize(source.getBytesNoCopy(), null)); } case Value.STRING: { assert source instanceof ValueString; return ValueComplex.get( ComplexNumber.parseComplexNumber(source.getString())); } case Value.BYTES: { assert source instanceof ValueBytes; return ValueComplex.get((ComplexNumber) JdbcUtils.deserialize(source.getBytesNoCopy(), null)); } case Value.DOUBLE: { assert source instanceof ValueDouble; return ValueComplex.get(new ComplexNumber(source.getDouble(), 0)); } } throw DbException.get( ErrorCode.DATA_CONVERSION_ERROR_1, source.getString()); } else { return source.convertTo(targetType); } } @Override public int getDataTypeOrder(int type) { if (type == COMPLEX_DATA_TYPE_ID) { return COMPLEX_DATA_TYPE_ORDER; } throw DbException.get( ErrorCode.UNKNOWN_DATA_TYPE_1, "type:" + type); } @Override public Value getValue(int type, Object data, DataHandler dataHandler) { if (type == COMPLEX_DATA_TYPE_ID) { assert data instanceof ComplexNumber; return ValueComplex.get((ComplexNumber)data); } return ValueJavaObject.getNoCopy(data, null, dataHandler); } @Override public Object getObject(Value value, Class<?> cls) { if (cls.equals(ComplexNumber.class)) { if (value.getType() == COMPLEX_DATA_TYPE_ID) { return value.getObject(); } return convert(value, COMPLEX_DATA_TYPE_ID).getObject(); } throw DbException.get( ErrorCode.UNKNOWN_DATA_TYPE_1, "type:" + value.getType()); } @Override public boolean supportsAdd(int type) { if (type == COMPLEX_DATA_TYPE_ID) { return true; } return false; } @Override public int getAddProofType(int type) { if (type == COMPLEX_DATA_TYPE_ID) { return type; } throw DbException.get( ErrorCode.UNKNOWN_DATA_TYPE_1, "type:" + type); } /** Constructs data type instance for complex number type */ private DataType createComplex() { DataType result = new DataType(); result.type = COMPLEX_DATA_TYPE_ID; result.name = COMPLEX_DATA_TYPE_NAME; result.sqlType = Types.JAVA_OBJECT; return result; } } /** * Value type implementation that holds the complex number */ public static class ValueComplex extends Value { private ComplexNumber val; /** * @param val complex number */ public ValueComplex(ComplexNumber val) { assert val != null; this.val = val; } /** * Get ValueComplex instance for given ComplexNumber. * * @param val complex number * @return resulting instance */ public static ValueComplex get(ComplexNumber val) { return new ValueComplex(val); } @Override public String getSQL() { return val.toString(); } @Override public int getType() { return TestOnlyCustomDataTypesHandler.COMPLEX_DATA_TYPE_ID; } @Override public long getPrecision() { return 0; } @Override public int getDisplaySize() { return 0; } @Override public String getString() { return val.toString(); } @Override public Object getObject() { return val; } @Override public void set(PreparedStatement prep, int parameterIndex) throws SQLException { Object obj = JdbcUtils.deserialize(getBytesNoCopy(), getDataHandler()); prep.setObject(parameterIndex, obj, Types.JAVA_OBJECT); } @Override protected int compareSecure(Value v, CompareMode mode) { return val.compare((ComplexNumber) v.getObject()); } @Override public int hashCode() { return val.hashCode(); } @Override public boolean equals(Object other) { if (other == null) { return false; } if (!(other instanceof ValueComplex)) { return false; } ValueComplex complex = (ValueComplex)other; return complex.val.equals(val); } @Override public Value convertTo(int targetType) { if (getType() == targetType) { return this; } switch (targetType) { case Value.BYTES: { return ValueBytes.getNoCopy(JdbcUtils.serialize(val, null)); } case Value.STRING: { return ValueString.get(val.toString()); } case Value.DOUBLE: { assert val.im == 0; return ValueDouble.get(val.re); } case Value.JAVA_OBJECT: { return ValueJavaObject.getNoCopy(JdbcUtils.serialize(val, null)); } } throw DbException.get( ErrorCode.DATA_CONVERSION_ERROR_1, getString()); } @Override public Value add(Value value) { ValueComplex v = (ValueComplex)value; return ValueComplex.get(val.add(v.val)); } } /** * Complex number */ public static class ComplexNumber implements Serializable { /** */ private static final long serialVersionUID = 1L; /** */ public final static DecimalFormat REAL_FMT = new DecimalFormat("###.###"); /** */ public final static DecimalFormat IMG_FMT = new DecimalFormat("+###.###i;-###.###i"); /** * Real part */ private double re; /** * Imaginary part */ private double im; /** * @param re real part * @param im imaginary part */ public ComplexNumber(double re, double im) { this.re = re; this.im = im; } /** * Addition * @param other value to add * @return result */ public ComplexNumber add(ComplexNumber other) { return new ComplexNumber(re + other.re, im + other.im); } /** * Returns modulus * @return result */ public double mod() { return Math.sqrt(re * re + im * im); } /** * Compares two complex numbers * * True ordering of complex number has no sense, * so we apply lexicographical order. * * @param v number to compare this with * @return result of comparison */ public int compare(ComplexNumber v) { if (re == v.re && im == v.im) { return 0; } if (re == v.re) { return im > v.im ? 1 : -1; } else if (re > v.re) { return 1; } else { return -1; } } @Override public int hashCode() { return (int)re | (int)im; } @Override public boolean equals(Object other) { if (other == null) { return false; } if (!(other instanceof ComplexNumber)) { return false; } ComplexNumber complex = (ComplexNumber)other; return (re==complex.re) && (im == complex.im); } @Override public String toString() { if (im == 0.0) { return REAL_FMT.format(re); } if (re == 0.0) { return IMG_FMT.format(im); } return REAL_FMT.format(re) + "" + IMG_FMT.format(im); } /** * Simple parser for complex numbers. Both real and im components * must be written in non scientific notation. * @param s String. * @return {@link ComplexNumber} object. */ public static ComplexNumber parseComplexNumber(String s) { if (StringUtils.isNullOrEmpty(s)) return null; s = s.replaceAll("\\s", ""); boolean hasIm = (s.charAt(s.length() - 1) == 'i'); int signs = 0; int pos = 0; int maxSignPos = -1; while (pos != -1) { pos = s.indexOf('-', pos); if (pos != -1) { signs++; maxSignPos = Math.max(maxSignPos, pos++); } } pos = 0; while (pos != -1) { pos = s.indexOf('+', pos); if (pos != -1) { signs++; maxSignPos = Math.max(maxSignPos, pos++); } } if (signs > 2 || (signs == 2 && !hasIm)) throw new NumberFormatException(); double real; double im; if (signs == 0 || (signs == 1 && maxSignPos == 0)) { if (hasIm) { real = 0; if (signs == 0 && s.length() == 1) { im = 1.0; } else if (signs > 0 && s.length() == 2) { im = (s.charAt(0) == '-') ? -1.0 : 1.0; } else { im = Double.parseDouble(s.substring(0, s.length() - 1)); } } else { real = Double.parseDouble(s); im = 0; } } else { real = Double.parseDouble(s.substring(0, maxSignPos)); if (s.length() - maxSignPos == 2) { im = (s.charAt(maxSignPos) == '-') ? -1.0 : 1.0; } else { im = Double.parseDouble(s.substring(maxSignPos, s.length() - 1)); } } return new ComplexNumber(real, im); } } }