From 37a791e6da32fa3ee4b683b36929494c7f504375 Mon Sep 17 00:00:00 2001 From: Roman Meier Date: Tue, 21 Dec 2021 22:32:18 +0100 Subject: [PATCH] add support for xmltype in postgres Signed-off-by: Meier Roman --- .../java/io/vertx/pgclient/data/PgSQLXML.java | 36 ++++++ .../java/io/vertx/pgclient/impl/RowImpl.java | 21 ++-- .../vertx/pgclient/impl/codec/DataType.java | 18 +-- .../pgclient/impl/codec/DataTypeCodec.java | 68 +++++++---- .../pgclient/data/PgSQLXMLCodecTest.java | 115 ++++++++++++++++++ 5 files changed, 217 insertions(+), 41 deletions(-) create mode 100644 vertx-pg-client/src/main/java/io/vertx/pgclient/data/PgSQLXML.java create mode 100644 vertx-pg-client/src/test/java/io/vertx/pgclient/data/PgSQLXMLCodecTest.java diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/data/PgSQLXML.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/data/PgSQLXML.java new file mode 100644 index 000000000..1c1ac53d8 --- /dev/null +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/data/PgSQLXML.java @@ -0,0 +1,36 @@ +package io.vertx.pgclient.data; + +import io.vertx.codegen.annotations.Nullable; + +import java.util.Objects; + +public class PgSQLXML { + + @Nullable final String xmlData; + + public PgSQLXML(String xmlData) { + this.xmlData = xmlData; + } + + public String getXmlData() { + return xmlData; + } + + @Override + public String toString() { + return xmlData; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PgSQLXML pgSQLXML = (PgSQLXML) o; + return Objects.equals(xmlData, pgSQLXML.xmlData); + } + + @Override + public int hashCode() { + return Objects.hash(xmlData); + } +} diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/RowImpl.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/RowImpl.java index 716919314..e9b8467e4 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/RowImpl.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/RowImpl.java @@ -19,16 +19,9 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.pgclient.data.Box; -import io.vertx.pgclient.data.Circle; -import io.vertx.pgclient.data.Line; -import io.vertx.pgclient.data.LineSegment; +import io.vertx.pgclient.data.*; import io.vertx.sqlclient.Row; import io.vertx.sqlclient.data.Numeric; -import io.vertx.pgclient.data.Path; -import io.vertx.pgclient.data.Polygon; -import io.vertx.pgclient.data.Interval; -import io.vertx.pgclient.data.Point; import io.vertx.sqlclient.impl.ArrayTuple; import io.vertx.sqlclient.impl.RowDesc; import io.vertx.core.buffer.Buffer; @@ -118,6 +111,8 @@ public T get(Class type, int position) { return type.cast(getArrayOfIntervals(position)); } else if (componentType == Box.class) { return type.cast(getArrayOfBoxs(position)); + } else if (componentType == PgSQLXML.class) { + return type.cast(getArrayOfPgXMLSQLs(position)); } else if (componentType == Object.class) { return type.cast(getJsonArray_(position)); } else if (componentType.isEnum()) { @@ -174,6 +169,8 @@ public T get(Class type, int position) { return type.cast(getJson(position)); } else if (type == JsonArray.class) { return type.cast(getJson(position)); + } else if (type == PgSQLXML.class) { + return type.cast(getPgSQLXML(position)); } else if (type == Object.class) { return type.cast(getValue(position)); } else if (type.isEnum()) { @@ -215,6 +212,10 @@ private Interval getInterval(int pos) { return (Interval) getValue(pos); } + private PgSQLXML getPgSQLXML(int pos) { + return (PgSQLXML) getValue(pos); + } + private Object getEnum(Class enumType, int pos) { Object val = getValue(pos); if (val instanceof String) { @@ -270,6 +271,10 @@ private Interval[] getArrayOfIntervals(int pos) { return (Interval[]) getValue(pos); } + private PgSQLXML[] getArrayOfPgXMLSQLs(int pos) { + return (PgSQLXML[]) getValue(pos); + } + private Object[] getArrayOfEnums(Class enumType, int pos) { Object val = getValue(pos); if (val instanceof String[]) { diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java index 0ac278820..b1baacb80 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataType.java @@ -22,18 +22,9 @@ import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; -import io.vertx.pgclient.data.Box; -import io.vertx.pgclient.data.Circle; -import io.vertx.pgclient.data.Inet; -import io.vertx.pgclient.data.Line; -import io.vertx.pgclient.data.LineSegment; -import io.vertx.pgclient.data.Money; +import io.vertx.pgclient.data.*; import io.vertx.sqlclient.Tuple; import io.vertx.sqlclient.data.Numeric; -import io.vertx.pgclient.data.Interval; -import io.vertx.pgclient.data.Path; -import io.vertx.pgclient.data.Point; -import io.vertx.pgclient.data.Polygon; import io.vertx.core.buffer.Buffer; import java.sql.JDBCType; @@ -105,8 +96,8 @@ public enum DataType { JSON_ARRAY(199, true, Object[].class, JDBCType.OTHER, Tuple::getArrayOfJsons), JSONB(3802, true, Object.class, JDBCType.OTHER, Tuple::getJson), JSONB_ARRAY(3807, true, Object[].class, JDBCType.OTHER, Tuple::getArrayOfJsons), - XML(142, true, Object.class, JDBCType.OTHER), - XML_ARRAY(143, true, Object[].class, JDBCType.OTHER), + XML(142, true, Object.class, JDBCType.SQLXML), + XML_ARRAY(143, true, Object[].class, JDBCType.SQLXML), POINT(600, true, Point.class, JDBCType.OTHER), POINT_ARRAY(1017, true, Point[].class, JDBCType.OTHER), LINE(628, true, Line.class, JDBCType.OTHER), @@ -229,5 +220,8 @@ static DataType lookup(Class type) { encodingTypeToDataType.put(Polygon[].class, POLYGON_ARRAY); encodingTypeToDataType.put(Circle.class, CIRCLE); encodingTypeToDataType.put(Circle[].class, CIRCLE_ARRAY); + + encodingTypeToDataType.put(PgSQLXML.class, XML); + encodingTypeToDataType.put(PgSQLXML[].class, XML_ARRAY); } } diff --git a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java index 2e0562eed..b8205695a 100644 --- a/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java +++ b/vertx-pg-client/src/main/java/io/vertx/pgclient/impl/codec/DataTypeCodec.java @@ -20,16 +20,16 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.DecoderException; +import io.vertx.core.buffer.Buffer; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.Json; -import io.vertx.sqlclient.Tuple; -import io.vertx.sqlclient.data.Numeric; -import io.vertx.pgclient.data.*; -import io.vertx.pgclient.impl.util.UTF8StringEndDetector; -import io.vertx.core.buffer.Buffer; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.pgclient.data.*; +import io.vertx.pgclient.impl.util.UTF8StringEndDetector; +import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.data.Numeric; import io.vertx.sqlclient.impl.codec.CommonCodec; import java.net.Inet4Address; @@ -50,7 +50,7 @@ import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME; -import static java.util.concurrent.TimeUnit.*; +import static java.util.concurrent.TimeUnit.NANOSECONDS; /** * @author Julien Viet @@ -91,6 +91,8 @@ public class DataTypeCodec { private static final OffsetDateTime OFFSET_DATE_TIME_EPOCH = LocalDateTime.of(2000, 1, 1, 0, 0, 0).atOffset(ZoneOffset.UTC); private static final Inet[] empty_inet_array = new Inet[0]; private static final Money[] empty_money_array = new Money[0]; + private static final PgSQLXML[] empty_pgsqlxml_array = new PgSQLXML[0]; + // Sentinel used when an object is refused by the data type public static final Object REFUSED_SENTINEL = new Object(); @@ -107,7 +109,7 @@ public class DataTypeCodec { private static final IntFunction OFFSETTIME_ARRAY_FACTORY = size -> size == 0 ? empty_offset_time_array : new OffsetTime[size]; private static final IntFunction LOCALDATETIME_ARRAY_FACTORY = size -> size == 0 ? empty_local_date_time_array : new LocalDateTime[size]; private static final IntFunction OFFSETDATETIME_ARRAY_FACTORY = size -> size == 0 ? empty_offset_date_time_array : new OffsetDateTime[size]; - private static final IntFunction BUFFER_ARRAY_FACTORY =size -> size == 0 ? empty_buffer_array : new Buffer[size]; + private static final IntFunction BUFFER_ARRAY_FACTORY = size -> size == 0 ? empty_buffer_array : new Buffer[size]; private static final IntFunction UUID_ARRAY_FACTORY = size -> size == 0 ? empty_uuid_array : new UUID[size]; private static final IntFunction JSON_ARRAY_FACTORY = size -> size == 0 ? empty_json_array : new Object[size]; private static final IntFunction NUMERIC_ARRAY_FACTORY = size -> size == 0 ? empty_numeric_array : new Numeric[size]; @@ -121,20 +123,18 @@ public class DataTypeCodec { private static final IntFunction INTERVAL_ARRAY_FACTORY = size -> size == 0 ? empty_interval_array : new Interval[size]; private static final IntFunction INET_ARRAY_FACTORY = size -> size == 0 ? empty_inet_array : new Inet[size]; private static final IntFunction MONEY_ARRAY_FACTORY = size -> size == 0 ? empty_money_array : new Money[size]; - + private static final IntFunction PGSQLXML_ARRAY_FACTORY = size -> size == 0 ? empty_pgsqlxml_array : new PgSQLXML[size]; private static final java.time.format.DateTimeFormatter TIMETZ_FORMAT = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_TIME) .appendOffset("+HH:mm", "00:00") .toFormatter(); - private static final java.time.format.DateTimeFormatter TIMESTAMP_FORMAT = new DateTimeFormatterBuilder() .parseCaseInsensitive() .append(ISO_LOCAL_DATE) .appendLiteral(' ') .append(ISO_LOCAL_TIME) .toFormatter(); - private static final java.time.format.DateTimeFormatter TIMESTAMPTZ_FORMAT = new DateTimeFormatterBuilder() .append(TIMESTAMP_FORMAT) .appendOffset("+HH:mm", "00:00") @@ -360,6 +360,12 @@ public static void encodeBinary(DataType id, Object value, ByteBuf buff) { case MONEY_ARRAY: binaryEncodeArray((Money[]) value, DataType.MONEY, buff); break; + case XML: + binaryEncodePgXMLSQL((PgSQLXML) value, buff); + break; + case XML_ARRAY: + binaryEncodeArray((PgSQLXML[]) value, DataType.XML, buff); + break; default: logger.debug("Data type " + id + " does not support binary encoding"); defaultEncodeBinary(value, buff); @@ -497,6 +503,10 @@ public static Object decodeBinary(DataType id, int index, int len, ByteBuf buff) return binaryDecodeMoney(index, len, buff); case MONEY_ARRAY: return binaryDecodeArray(MONEY_ARRAY_FACTORY, DataType.MONEY, index, len, buff); + case XML: + return binaryDecodePgXMLSQL(index, len, buff); + case XML_ARRAY: + return binaryDecodeArray(PGSQLXML_ARRAY_FACTORY, DataType.XML, index, len, buff); default: logger.debug("Data type " + id + " does not support binary decoding"); return defaultDecodeBinary(index, len, buff); @@ -637,6 +647,10 @@ public static Object decodeText(DataType id, int index, int len, ByteBuf buff) { return textDecodeMoney(index, len, buff); case MONEY_ARRAY: return textDecodeArray(MONEY_ARRAY_FACTORY, DataType.MONEY, index, len, buff); + case XML: + return textDecodePgSQLXML(index, len, buff); + case XML_ARRAY: + return textDecodeArray(PGSQLXML_ARRAY_FACTORY, DataType.XML, index, len, buff); default: return defaultDecodeText(index, len, buff); } @@ -669,7 +683,7 @@ private static Boolean binaryDecodeBOOL(int index, int len, ByteBuf buff) { } private static Boolean textDecodeBOOL(int index, int len, ByteBuf buff) { - if(buff.getByte(index) == 't') { + if (buff.getByte(index) == 't') { return Boolean.TRUE; } else { return Boolean.FALSE; @@ -770,7 +784,7 @@ private static Line textDecodeLine(int index, int len, ByteBuf buff) { private static LineSegment textDecodeLseg(int index, int len, ByteBuf buff) { // Lseg representation: [p1,p2] - int idxOfPointsSeparator = buff.indexOf(index, index+len, (byte) ')') + 1; + int idxOfPointsSeparator = buff.indexOf(index, index + len, (byte) ')') + 1; int lenOfP1 = idxOfPointsSeparator - index - 1; Point p1 = textDecodePOINT(index + 1, lenOfP1, buff); Point p2 = textDecodePOINT(idxOfPointsSeparator + 1, len - lenOfP1 - 3, buff); @@ -779,7 +793,7 @@ private static LineSegment textDecodeLseg(int index, int len, ByteBuf buff) { private static Box textDecodeBox(int index, int len, ByteBuf buff) { // Box representation: p1,p2 - int idxOfPointsSeparator = buff.indexOf(index, index+len, (byte) ')') + 1; + int idxOfPointsSeparator = buff.indexOf(index, index + len, (byte) ')') + 1; int lenOfUpperRightCornerPoint = idxOfPointsSeparator - index; Point upperRightCorner = textDecodePOINT(index, lenOfUpperRightCornerPoint, buff); Point lowerLeftCorner = textDecodePOINT(idxOfPointsSeparator + 1, len - lenOfUpperRightCornerPoint - 1, buff); @@ -905,7 +919,7 @@ private static Interval textDecodeINTERVAL(int index, int len, ByteBuf buff) { : Integer.parseInt(timeChunk.substring(sidx)); } else { // seconds with microseconds - seconds = isNeg ? -Integer.parseInt(timeChunk.substring(sidx).substring(0, m)) + seconds = isNeg ? -Integer.parseInt(timeChunk.substring(sidx).substring(0, m)) : Integer.parseInt(timeChunk.substring(sidx).substring(0, m)); microseconds = isNeg ? -Integer.parseInt(timeChunk.substring(sidx).substring(m + 1)) : Integer.parseInt(timeChunk.substring(sidx).substring(m + 1)); @@ -990,7 +1004,6 @@ private static String textDecodeNAME(int index, int len, ByteBuf buff) { return buff.getCharSequence(index, len, StandardCharsets.UTF_8).toString(); } - private static void binaryEncodeNAME(String value, ByteBuf buff) { String s = String.valueOf(value); buff.writeCharSequence(s, StandardCharsets.UTF_8); @@ -1466,7 +1479,7 @@ private static void binaryEncodeMoney(Money money, ByteBuf buff) { private static Money binaryDecodeMoney(int index, int len, ByteBuf buff) { long value = binaryDecodeINT8(index, len, buff); - return new Money(value / 100, Math.abs(((int)value % 100))); + return new Money(value / 100, Math.abs(((int) value % 100))); } private static String binaryDecodeTsQuery(int index, int len, ByteBuf buff) { @@ -1477,6 +1490,19 @@ private static void binaryEncodeTsQuery(String value, ByteBuf buff) { buff.writeCharSequence(String.valueOf(value), StandardCharsets.UTF_8); } + private static PgSQLXML binaryDecodePgXMLSQL(int index, int len, ByteBuf buff) { + return new PgSQLXML(buff.getCharSequence(index, len, StandardCharsets.UTF_8).toString()); + } + + private static void binaryEncodePgXMLSQL(PgSQLXML value, ByteBuf buff) { + buff.writeCharSequence(value.toString(), StandardCharsets.UTF_8); + } + + private static PgSQLXML textDecodePgSQLXML(int index, int len, ByteBuf buff) { + String s = textDecodeVARCHAR(index, len, buff); + return new PgSQLXML(s); + } + private static String textDecodeTsVector(int index, int len, ByteBuf buff) { return buff.getCharSequence(index, len, StandardCharsets.UTF_8).toString(); } @@ -1535,7 +1561,7 @@ private static Money textDecodeMoney(int index, int len, ByteBuf buff) { * Decode the specified {@code buff} formatted as an hex string starting at the buffer readable index * with the specified {@code length} to a {@link Buffer}. * - * @param len the hex string length + * @param len the hex string length * @param buff the byte buff to read from * @return the decoded value as a Buffer */ @@ -1551,7 +1577,7 @@ private static Buffer decodeHexStringToBytes(int index, int len, ByteBuf buff) { } private static byte decodeHexChar(byte ch) { - return (byte)(((ch & 0x1F) + ((ch >> 6) * 0x19) - 0x10) & 0x0F); + return (byte) (((ch & 0x1F) + ((ch >> 6) * 0x19) - 0x10) & 0x0F); } private static boolean isHexFormat(int index, int len, ByteBuf buff) { @@ -1619,7 +1645,7 @@ private static T[] binaryDecodeArray(IntFunction supplier, DataType typ return array; } - private static void binaryEncodeArray(T[] values, DataType type, ByteBuf buff){ + private static void binaryEncodeArray(T[] values, DataType type, ByteBuf buff) { int startIndex = buff.writerIndex(); buff.writeInt(1); // ndim buff.writeInt(0); // dataoffset @@ -1681,7 +1707,7 @@ private static T textDecodeArrayElement(DataType type, int index, int len, B // Some escaping - improve that later... String s = buff.toString(index + 1, len - 2, StandardCharsets.UTF_8); StringBuilder sb = new StringBuilder(); - for (int i = 0;i < s.length();i++) { + for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c == '\\') { c = s.charAt(++i); @@ -1696,7 +1722,7 @@ private static T textDecodeArrayElement(DataType type, int index, int len, B } } - private static void textEncodeArray(T[] values, DataType type, ByteBuf buff){ + private static void textEncodeArray(T[] values, DataType type, ByteBuf buff) { buff.writeByte('{'); int len = values.length; for (int i = 0; i < len; i++) { diff --git a/vertx-pg-client/src/test/java/io/vertx/pgclient/data/PgSQLXMLCodecTest.java b/vertx-pg-client/src/test/java/io/vertx/pgclient/data/PgSQLXMLCodecTest.java new file mode 100644 index 000000000..4575e56ec --- /dev/null +++ b/vertx-pg-client/src/test/java/io/vertx/pgclient/data/PgSQLXMLCodecTest.java @@ -0,0 +1,115 @@ +package io.vertx.pgclient.data; + +import io.vertx.ext.unit.TestContext; +import io.vertx.pgclient.PgConnection; +import io.vertx.pgclient.PgException; +import io.vertx.sqlclient.*; +import org.junit.Test; + +import java.util.function.BiFunction; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public class PgSQLXMLCodecTest extends DataTypeTestBase { + + @Test + public void testBinaryEncodePgSQLXMLAsVarcharOrXML(TestContext ctx) { + PgSQLXML asVarchar = new PgSQLXML(""); + PgSQLXML asXML = new PgSQLXML(""); + + PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("SELECT ($1::xml)::VARCHAR, ($2::xml)").execute(Tuple.of(asVarchar, asXML), + ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + String v1 = row.getString(0); + PgSQLXML v2 = (PgSQLXML)row.getValue(1); + ctx.assertEquals("", v1); + ctx.assertEquals(asXML, v2); + }) + ); + })); + } + + @Test + public void testBinaryEncodePgSQLXMLMalformed(TestContext ctx) { + PgSQLXML malformedXml = new PgSQLXML(">>"); + + PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("SELECT ($1::xml)").execute(Tuple.of(malformedXml), + ctx.asyncAssertFailure(err -> { + assertThat(((PgException) err).getCode(), is(equalTo("2200N"))); + }) + ); + })); + } + + @Test + public void testTextDecodePgSQLXML(TestContext ctx) { + testDecodePgSQLXML(ctx, SqlClient::query); + } + + @Test + public void testBinaryDecodePgSQLXML(TestContext ctx) { + testDecodePgSQLXML(ctx, SqlClient::preparedQuery); + } + + private void testDecodePgSQLXML(TestContext ctx, BiFunction>> a) { + PgSQLXML first = new PgSQLXML(""); + + PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + a.apply(conn, "SELECT ''::xml") + .execute(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + PgSQLXML v1 = (PgSQLXML) row.getValue(0); + ctx.assertEquals(first, v1); + })); + })); + } + + @Test + public void testBinaryDecodePgSQLXMLArray(TestContext ctx) throws Exception { + PgSQLXML first = new PgSQLXML(""); + PgSQLXML second = new PgSQLXML(""); + + PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("SELECT ARRAY[''::xml,''::xml]") + .execute(ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + PgSQLXML[] array = (PgSQLXML[]) row.getValue(0); + PgSQLXML v1 = array[0]; + PgSQLXML v2 = array[1]; + ctx.assertEquals(first, v1); + ctx.assertEquals(second, v2); + })); + })); + } + + @Test + public void testBinaryEncodePgSQLXMLArray(TestContext ctx) { + PgSQLXML first = new PgSQLXML(""); + PgSQLXML second = new PgSQLXML(""); + + + PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> { + conn.preparedQuery("SELECT ($1::xml[])::VARCHAR[]").execute(Tuple.of( + new PgSQLXML[]{ + new PgSQLXML(""), + new PgSQLXML("")} + ), + ctx.asyncAssertSuccess(rows -> { + ctx.assertEquals(1, rows.size()); + Row row = rows.iterator().next(); + String[] array = row.getArrayOfStrings(0); + String v1 = array[0]; + String v2 = array[1]; + ctx.assertEquals(first.getXmlData(), v1); + ctx.assertEquals(second.getXmlData(), v2); + })); + })); + } +}