From 38ec412e61b72112e88e5a6311a27a365ace9968 Mon Sep 17 00:00:00 2001 From: Jan Schatteman Date: Thu, 2 Feb 2023 16:06:36 +0100 Subject: [PATCH] HHH-15665 - Fix and added test for issue Signed-off-by: Jan Schatteman --- .../boot/model/naming/Identifier.java | 72 ++++++++++++++ ...formationExtractorMariaDBDatabaseImpl.java | 5 +- ...MariaDBExtractSequenceInformationTest.java | 98 +++++++++++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/dialect/functional/MariaDBExtractSequenceInformationTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/Identifier.java b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/Identifier.java index be697f9be4..667c189bf5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/Identifier.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/Identifier.java @@ -75,6 +75,64 @@ public class Identifier implements Comparable { } } + /** + * Means to generate an {@link Identifier} instance from its simple text form. + *

+ * If passed text is {@code null}, {@code null} is returned. + *

+ * If passed text is surrounded in quote markers, the generated Identifier + * is considered quoted. Quote markers include back-ticks (`), + * double-quotes (") and brackets ([ and ]). + * + * @param text The text form + * @param quote Whether to quote unquoted text forms + * @param quoteOnNonIdentifierChar Controls whether to treat the result as quoted if text contains characters that are invalid for identifiers + * + * @return The identifier form, or {@code null} if text was {@code null} + */ + public static Identifier toIdentifier(String text, boolean quote, boolean quoteOnNonIdentifierChar) { + if ( StringHelper.isEmpty( text ) ) { + return null; + } + int start = 0; + int end = text.length(); + while ( start < end ) { + if ( !Character.isWhitespace( text.charAt( start ) ) ) { + break; + } + start++; + } + while ( start < end ) { + if ( !Character.isWhitespace( text.charAt( end - 1 ) ) ) { + break; + } + end--; + } + if ( isQuoted( text, start, end ) ) { + start++; + end--; + quote = true; + } + else if ( quoteOnNonIdentifierChar && !quote ) { + // Check the letters to determine if we must quote the text + char c = text.charAt( start ); + if ( !Character.isLetter( c ) && c != '_' ) { + // SQL identifiers must begin with a letter or underscore + quote = true; + } + else { + for ( int i = start + 1; i < end; i++ ) { + c = text.charAt( i ); + if ( !Character.isLetterOrDigit( c ) && c != '_' ) { + quote = true; + break; + } + } + } + } + return new Identifier( text.substring( start, end ), quote ); + } + /** * Is the given identifier text considered quoted. The following patterns are * recognized as quoted:

    @@ -96,6 +154,20 @@ public class Identifier implements Comparable { || ( name.startsWith( "\"" ) && name.endsWith( "\"" ) ); } + public static boolean isQuoted(String name, int start, int end) { + if ( start + 2 < end ) { + switch ( name.charAt( start ) ) { + case '`': + return name.charAt( end - 1 ) == '`'; + case '[': + return name.charAt( end - 1 ) == ']'; + case '"': + return name.charAt( end - 1 ) == '"'; + } + } + return false; + } + /** * Constructs an identifier instance. * diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/SequenceInformationExtractorMariaDBDatabaseImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/SequenceInformationExtractorMariaDBDatabaseImpl.java index e58f5fb196..5289eb8c8f 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/SequenceInformationExtractorMariaDBDatabaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/extract/internal/SequenceInformationExtractorMariaDBDatabaseImpl.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.QualifiedSequenceName; import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.tool.schema.extract.spi.ExtractionContext; @@ -28,7 +29,7 @@ public class SequenceInformationExtractorMariaDBDatabaseImpl extends SequenceInf // SQL to get metadata from individual sequence private static final String SQL_SEQUENCE_QUERY = - "SELECT '%1$s' as sequence_name, minimum_value, maximum_value, start_value, increment, cache_size FROM %1$s "; + "SELECT '%1$s' as sequence_name, minimum_value, maximum_value, start_value, increment, cache_size FROM %2$s "; private static final String UNION_ALL = "UNION ALL "; @@ -56,7 +57,7 @@ public class SequenceInformationExtractorMariaDBDatabaseImpl extends SequenceInf if ( sequenceInfoQueryBuilder.length() > 0 ) { sequenceInfoQueryBuilder.append( UNION_ALL ); } - sequenceInfoQueryBuilder.append( String.format( SQL_SEQUENCE_QUERY, sequenceName ) ); + sequenceInfoQueryBuilder.append( String.format( SQL_SEQUENCE_QUERY, sequenceName, Identifier.toIdentifier( sequenceName, false, true ) ) ); } return extractionContext.getQueryResults( sequenceInfoQueryBuilder.toString(), diff --git a/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/MariaDBExtractSequenceInformationTest.java b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/MariaDBExtractSequenceInformationTest.java new file mode 100644 index 0000000000..0f91ec8b21 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/dialect/functional/MariaDBExtractSequenceInformationTest.java @@ -0,0 +1,98 @@ +package org.hibernate.test.dialect.functional; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.stream.StreamSupport; + +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorMariaDBDatabaseImpl; +import org.hibernate.tool.schema.extract.spi.ExtractionContext; +import org.hibernate.tool.schema.extract.spi.SequenceInformation; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInAutoCommit; + +/** + * @author Jan Schatteman + */ +@RequiresDialect(value = MariaDBDialect.class) +public class MariaDBExtractSequenceInformationTest extends BaseCoreFunctionalTestCase { + + private final static String hhh15665SeqName = "HHH-15665-seq"; + + private final static Map settings = new HashMap<>(3); + + static { + settings.put( AvailableSettings.URL, Environment.getProperties().getProperty( AvailableSettings.URL ) ); + settings.put( AvailableSettings.USER, Environment.getProperties().getProperty( AvailableSettings.USER ) ); + settings.put( AvailableSettings.PASS, Environment.getProperties().getProperty( AvailableSettings.PASS ) ); + } + + @BeforeClass + public static void setUp() throws Exception { + doInAutoCommit( settings, "CREATE SEQUENCE IF NOT EXISTS `" + hhh15665SeqName + "`" ); + } + + @AfterClass + public static void tearDown() throws SQLException { + doInAutoCommit( settings, "DROP SEQUENCE IF EXISTS `" + hhh15665SeqName + "`" ); + } + + @Test + @TestForIssue( jiraKey = "HHH-15665" ) + public void testExtractSequenceInformationForSqlServerWithCaseSensitiveCollation() { + StandardServiceRegistry ssr = new StandardServiceRegistryBuilder().applySettings( settings ).build(); + JdbcEnvironment jdbcEnvironment = ssr.getService( JdbcEnvironment.class ); + JdbcConnectionAccess bootstrapJdbcConnectionAccess = ssr.getService( JdbcServices.class ).getBootstrapJdbcConnectionAccess(); + + try ( Connection connection = bootstrapJdbcConnectionAccess.obtainConnection() ) { + Iterable sequenceInformations = SequenceInformationExtractorMariaDBDatabaseImpl.INSTANCE.extractMetadata( + new ExtractionContext.EmptyExtractionContext() { + @Override + public Connection getJdbcConnection() { + return connection; + } + + @Override + public JdbcEnvironment getJdbcEnvironment() { + return jdbcEnvironment; + } + } ); + + Assert.assertNotNull( sequenceInformations ); + + Optional seq = StreamSupport.stream( sequenceInformations.spliterator(), false ) + .filter( + sequence -> hhh15665SeqName.equals( sequence.getSequenceName() + .getSequenceName() + .getText() ) + ) + .findFirst(); + + Assert.assertTrue( hhh15665SeqName + " not found", seq.isPresent() ); + } + catch ( SQLException e ) { + Assert.fail( "Sequence information for " + hhh15665SeqName + " was not retrieved: " + e.getMessage() ); + } + finally { + StandardServiceRegistryBuilder.destroy( ssr ); + } + } +}