From ddaff288386346563182f6229a9df98d5d8026d5 Mon Sep 17 00:00:00 2001 From: gavinking Date: Sun, 2 Feb 2020 03:57:52 +0100 Subject: [PATCH] Add hexadecimal binary literals with standard SQL syntax And fix up the parsing and formatting in VarbinaryTypeDescriptor which was inconsistent with how the JDBC drivers handle the conversion to signed bytes. --- .../org/hibernate/grammars/hql/HqlLexer.g4 | 4 +++ .../org/hibernate/grammars/hql/HqlParser.g4 | 1 + .../java/org/hibernate/dialect/Dialect.java | 4 +++ .../org/hibernate/dialect/OracleDialect.java | 5 ++++ .../hibernate/dialect/PostgreSQLDialect.java | 5 ++++ .../hibernate/dialect/SQLServerDialect.java | 5 ++++ .../hql/internal/SemanticQueryBuilder.java | 11 ++++++++ .../java/ByteArrayTypeDescriptor.java | 4 +-- .../PrimitiveByteArrayTypeDescriptor.java | 6 ++-- .../descriptor/sql/JdbcLiteralFormatter.java | 1 + .../sql/VarbinaryTypeDescriptor.java | 7 +++++ .../internal/JdbcLiteralFormatterBinary.java | 28 +++++++++++++++++++ .../orm/test/query/hql/LiteralTests.java | 13 +++++++++ 13 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/JdbcLiteralFormatterBinary.java diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 index 43a45577a3..05cb7ddd99 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlLexer.g4 @@ -93,6 +93,10 @@ UNICODE_ESCAPE : 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT ; +BINARY_LITERAL + : [xX] SINGLE_QUOTE (HEX_DIGIT HEX_DIGIT)* SINGLE_QUOTE + ; + // ESCAPE start tokens TIMESTAMP_ESCAPE_START : '{ts'; DATE_ESCAPE_START : '{d'; diff --git a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 index 201e59e193..16e28bf86e 100644 --- a/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 +++ b/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4 @@ -501,6 +501,7 @@ nullifFunction literal : STRING_LITERAL + | BINARY_LITERAL | INTEGER_LITERAL | LONG_LITERAL | BIG_INTEGER_LITERAL diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index d7fde2449c..fddabca99e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -3285,6 +3285,10 @@ public abstract class Dialect implements ConversionContext { return true; } + public String formatBinaryliteral(byte[] bytes) { + return "X'" + StandardBasicTypes.BINARY.toString( bytes ) + "'"; + } + /** * Pluggable strategy for determining the Size to use for columns of * a given SQL type when no explicit Size has been given. diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 65c2c7b8d6..a5f5a9d1d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -1085,6 +1085,11 @@ public class OracleDialect extends Dialect { .replace("x", "TZH"); //note special case } + @Override + public String formatBinaryliteral(byte[] bytes) { + return "hextoraw('" + StandardBasicTypes.BINARY.toString( bytes ) + "')"; + } + @Override public ResultSet getResultSet(CallableStatement statement, int position) throws SQLException { return (ResultSet) statement.getObject( position ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 9e2025d1d1..e443f6276c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -691,6 +691,11 @@ public class PostgreSQLDialect extends Dialect { } } + @Override + public String formatBinaryliteral(byte[] bytes) { + return "bytea '\\x" + StandardBasicTypes.BINARY.toString( bytes ) + "'"; + } + @Override protected String wrapDateLiteral(String date) { return wrapAsAnsiDateLiteral(date); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 7723351753..cdb1e63fe6 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -584,6 +584,11 @@ public class SQLServerDialect extends AbstractTransactSQLDialect { private static final Pattern OFFSET_PATTERN = compile(".*[-+]\\d{2}(:\\d{2})?$"); + @Override + public String formatBinaryliteral(byte[] bytes) { + return "0x" + StandardBasicTypes.BINARY.toString( bytes ); + } + @Override protected String wrapTimestampLiteral(String timestamp) { //needed because the {ts ... } JDBC escape chokes on microseconds diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index c8631edd25..ed3b700336 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java @@ -1746,6 +1746,9 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implements SqmCre if ( ctx.literal().STRING_LITERAL() != null ) { return stringLiteral( ctx.literal().STRING_LITERAL().getText() ); } + else if ( ctx.literal().BINARY_LITERAL() != null ) { + return binaryLiteral( ctx.literal().BINARY_LITERAL().getText() ); + } else if ( ctx.literal().INTEGER_LITERAL() != null ) { return integerLiteral( ctx.literal().INTEGER_LITERAL().getText() ); } @@ -2034,6 +2037,14 @@ public class SemanticQueryBuilder extends HqlParserBaseVisitor implements SqmCre ); } + private SqmLiteral binaryLiteral(String text) { + return new SqmLiteral( + StandardBasicTypes.BINARY.fromStringValue( text.substring( 2, text.length()-1 ) ), + resolveExpressableTypeBasic( byte[].class ), + creationContext.getNodeBuilder() + ); + } + private SqmLiteral integerLiteral(String text) { try { final Integer value = Integer.valueOf( text ); diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ByteArrayTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ByteArrayTypeDescriptor.java index 465837be3e..f60ef6bbce 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ByteArrayTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/ByteArrayTypeDescriptor.java @@ -48,7 +48,7 @@ public class ByteArrayTypeDescriptor extends AbstractTypeDescriptor { public String toString(Byte[] bytes) { final StringBuilder buf = new StringBuilder(); for ( Byte aByte : bytes ) { - final String hexStr = Integer.toHexString( aByte - Byte.MIN_VALUE ); + final String hexStr = Integer.toHexString( Byte.toUnsignedInt(aByte) ); if ( hexStr.length() == 1 ) { buf.append( '0' ); } @@ -67,7 +67,7 @@ public class ByteArrayTypeDescriptor extends AbstractTypeDescriptor { Byte[] bytes = new Byte[string.length() / 2]; for ( int i = 0; i < bytes.length; i++ ) { final String hexStr = string.substring( i * 2, (i + 1) * 2 ); - bytes[i] = (byte) ( Integer.parseInt( hexStr, 16 ) + Byte.MIN_VALUE ); + bytes[i] = (byte) Integer.parseInt( hexStr, 16 ); } return bytes; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/PrimitiveByteArrayTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/PrimitiveByteArrayTypeDescriptor.java index 95f97f3147..1d9d7ea63a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/PrimitiveByteArrayTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/PrimitiveByteArrayTypeDescriptor.java @@ -49,7 +49,7 @@ public class PrimitiveByteArrayTypeDescriptor extends AbstractTypeDescriptor { String NULL = "null"; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/VarbinaryTypeDescriptor.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/VarbinaryTypeDescriptor.java index 510e961ef7..e52219d6b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/VarbinaryTypeDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/VarbinaryTypeDescriptor.java @@ -17,6 +17,7 @@ import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicJavaDescriptor; import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.internal.JdbcLiteralFormatterBinary; import org.hibernate.type.spi.TypeConfiguration; /** @@ -44,6 +45,12 @@ public class VarbinaryTypeDescriptor implements SqlTypeDescriptor { return (BasicJavaDescriptor) typeConfiguration.getJavaTypeDescriptorRegistry().getDescriptor( byte[].class ); } + @Override + public JdbcLiteralFormatter getJdbcLiteralFormatter(JavaTypeDescriptor javaTypeDescriptor) { + //noinspection unchecked + return new JdbcLiteralFormatterBinary( javaTypeDescriptor ); + } + public ValueBinder getBinder(final JavaTypeDescriptor javaTypeDescriptor) { return new BasicBinder( javaTypeDescriptor, this ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/JdbcLiteralFormatterBinary.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/JdbcLiteralFormatterBinary.java new file mode 100644 index 0000000000..008bfb1d39 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/sql/internal/JdbcLiteralFormatterBinary.java @@ -0,0 +1,28 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.type.descriptor.sql.internal; + +import org.hibernate.dialect.Dialect; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.sql.spi.BasicJdbcLiteralFormatter; + +/** + * JdbcLiteralFormatter implementation for handling binary literals + * + * @author Gavin King + */ +public class JdbcLiteralFormatterBinary extends BasicJdbcLiteralFormatter { + public JdbcLiteralFormatterBinary(JavaTypeDescriptor javaTypeDescriptor) { + super( javaTypeDescriptor ); + } + + @Override + public String toJdbcLiteral(Object value, Dialect dialect, SharedSessionContractImplementor session) { + return dialect.formatBinaryliteral( unwrap( value, byte[].class, session ) ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java index 2369fe0f3b..98922dacf5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java @@ -18,6 +18,7 @@ import java.math.BigInteger; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hibernate.type.StandardBasicTypes.BINARY; /** * @author Steve Ebersole @@ -28,6 +29,18 @@ import static org.hamcrest.Matchers.is; @SessionFactory public class LiteralTests { + @Test + public void testBinaryLiteral(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + byte[] bytes1 = (byte[]) session.createQuery( "select X'DEADBEEF'" ).getSingleResult(); + assertThat( BINARY.toString(bytes1), is("deadbeef") ); + byte[] bytes2 = (byte[]) session.createQuery( "select X'deadbeef'" ).getSingleResult(); + assertThat( BINARY.toString(bytes2), is("deadbeef") ); + } + ); + } + @Test public void testJdbcTimeLiteral(SessionFactoryScope scope) { scope.inTransaction(