diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java index 6da13df442..638b8a5d83 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDatabase.java @@ -211,6 +211,23 @@ public enum CommunityDatabase { public boolean productNameMatches(String databaseName) { return databaseName.toLowerCase().startsWith( "timesten" ); } + }, + + SINGLESTORE { + @Override + public Dialect createDialect(DialectResolutionInfo info) { + return new SingleStoreDialect( info ); + } + + @Override + public boolean productNameMatches(String databaseName) { + return databaseName.toLowerCase().startsWith( "singlestore" ); + } + + @Override + public String getDriverClassName(String jdbcUrl) { + return "com.singlestore.jdbc.Driver"; + } }; /** diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDialectSelector.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDialectSelector.java index 747d654b1c..9e80a52fb1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDialectSelector.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CommunityDialectSelector.java @@ -118,6 +118,8 @@ public class CommunityDialectSelector implements DialectSelector { return Teradata14Dialect.class; case "TimesTen": return TimesTenDialect.class; + case "SingleStore": + return SingleStoreDialect.class; } return null; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java new file mode 100644 index 0000000000..8af3ae96b9 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java @@ -0,0 +1,1414 @@ +/* + * 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 . + */ +package org.hibernate.community.dialect; + +import java.sql.CallableStatement; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.time.ZonedDateTime; +import java.time.temporal.TemporalAccessor; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +import org.hibernate.Length; +import org.hibernate.PessimisticLockException; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.relational.Exportable; +import org.hibernate.boot.model.relational.Sequence; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.dialect.DatabaseVersion; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.FunctionalDependencyAnalysisSupport; +import org.hibernate.dialect.FunctionalDependencyAnalysisSupportImpl; +import org.hibernate.dialect.NullOrdering; +import org.hibernate.dialect.Replacer; +import org.hibernate.dialect.SelectItemReferenceStrategy; +import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.hint.IndexQueryHintHandler; +import org.hibernate.dialect.identity.IdentityColumnSupport; +import org.hibernate.dialect.identity.MySQLIdentityColumnSupport; +import org.hibernate.dialect.pagination.LimitHandler; +import org.hibernate.dialect.pagination.LimitLimitHandler; +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.dialect.temptable.TemporaryTableKind; +import org.hibernate.dialect.unique.UniqueDelegate; +import org.hibernate.engine.jdbc.Size; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.IdentifierCaseStrategy; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; +import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; +import org.hibernate.engine.jdbc.env.spi.SchemaNameResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.exception.ConstraintViolationException; +import org.hibernate.exception.LockAcquisitionException; +import org.hibernate.exception.LockTimeoutException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; +import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; +import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; +import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.internal.util.collections.ArrayHelper; +import org.hibernate.internal.util.config.ConfigurationHelper; +import org.hibernate.mapping.Column; +import org.hibernate.mapping.ForeignKey; +import org.hibernate.mapping.Table; +import org.hibernate.mapping.UniqueKey; +import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.query.sqm.CastType; +import org.hibernate.query.sqm.IntervalType; +import org.hibernate.query.sqm.TemporalUnit; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.query.sqm.mutation.internal.temptable.AfterUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.BeforeUseAction; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableInsertStrategy; +import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.produce.function.FunctionParameterType; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.NullType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.EnumJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.NullJdbcType; +import org.hibernate.type.descriptor.jdbc.OrdinalEnumJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; +import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.NativeEnumDdlTypeImpl; +import org.hibernate.type.descriptor.sql.internal.NativeOrdinalEnumDdlTypeImpl; +import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; + +import jakarta.persistence.TemporalType; + +import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC; +import static org.hibernate.type.SqlTypes.BIGINT; +import static org.hibernate.type.SqlTypes.BINARY; +import static org.hibernate.type.SqlTypes.BIT; +import static org.hibernate.type.SqlTypes.BLOB; +import static org.hibernate.type.SqlTypes.BOOLEAN; +import static org.hibernate.type.SqlTypes.CHAR; +import static org.hibernate.type.SqlTypes.CLOB; +import static org.hibernate.type.SqlTypes.DECIMAL; +import static org.hibernate.type.SqlTypes.DOUBLE; +import static org.hibernate.type.SqlTypes.FLOAT; +import static org.hibernate.type.SqlTypes.INTEGER; +import static org.hibernate.type.SqlTypes.JSON; +import static org.hibernate.type.SqlTypes.LONG32NVARCHAR; +import static org.hibernate.type.SqlTypes.LONG32VARBINARY; +import static org.hibernate.type.SqlTypes.LONG32VARCHAR; +import static org.hibernate.type.SqlTypes.NCHAR; +import static org.hibernate.type.SqlTypes.NCLOB; +import static org.hibernate.type.SqlTypes.NVARCHAR; +import static org.hibernate.type.SqlTypes.SMALLINT; +import static org.hibernate.type.SqlTypes.TIMESTAMP; +import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TIME_WITH_TIMEZONE; +import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.VARBINARY; +import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMillis; + +/** + * An SQL dialect for SingleStore. + *

+ * The following are some of the key aspects and limitations of SingleStore that may affect Hibernate functionality: + *

+ * + * + * @author Oleksandr Yeliseiev + */ +public class SingleStoreDialect extends Dialect { + + private static final int PARAM_LIST_SIZE_LIMIT = 1_048_576; + private static final EmptyExporter NOOP_EXPORTER = new EmptyExporter(); + private static final UniqueDelegate NOOP_UNIQUE_DELEGATE = new DoNothingUniqueDelegate(); + private static final DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 8, 0 ); + + private final SingleStoreTableType explicitTableType; + private final boolean isForUpdateLockingEnabled; + + public SingleStoreDialect() { + this( MINIMUM_VERSION, null, false ); + } + + public SingleStoreDialect(DialectResolutionInfo info) { + this( createVersion( info ), getTableType( info ), getUpdateForEnabled( info ) ); + registerKeywords( info ); + } + + public SingleStoreDialect( + DatabaseVersion version, SingleStoreTableType explicitTableType, boolean isForUpdateLockingEnabled) { + super( version ); + this.explicitTableType = explicitTableType; + this.isForUpdateLockingEnabled = isForUpdateLockingEnabled; + } + + private static DatabaseVersion createVersion(DialectResolutionInfo info) { + final String versionString = info.getDatabaseVersion(); + if ( versionString != null ) { + final String[] components = versionString.split( "\\." ); + if ( components.length >= 3 ) { + try { + final int majorVersion = Integer.parseInt( components[0] ); + final int minorVersion = Integer.parseInt( components[1] ); + final int patchLevel = Integer.parseInt( components[2] ); + return DatabaseVersion.make( majorVersion, minorVersion, patchLevel ); + } + catch (NumberFormatException ex) { + // Ignore + } + } + } + return info.makeCopyOrDefault( MINIMUM_VERSION ); + } + + private static SingleStoreTableType getTableType(DialectResolutionInfo info) { + String value = ConfigurationHelper.getString( SINGLE_STORE_TABLE_TYPE, info.getConfigurationValues() ); + return value == null ? null : SingleStoreTableType.fromValue( value ); + } + + private static boolean getUpdateForEnabled(DialectResolutionInfo info) { + return ConfigurationHelper.getBoolean( + SINGLE_STORE_FOR_UPDATE_LOCK_ENABLED, + info.getConfigurationValues(), + false + ); + } + + private final SizeStrategy sizeStrategy = new SizeStrategyImpl() { + @Override + public Size resolveSize( + JdbcType jdbcType, JavaType javaType, Integer precision, Integer scale, Long length) { + switch ( jdbcType.getDdlTypeCode() ) { + case BIT: + if ( length != null ) { + return Size.length( Math.min( Math.max( length, 1 ), 64 ) ); + } + case BLOB: + case NCLOB: + case CLOB: + return super.resolveSize( + jdbcType, + javaType, + precision, + scale, + length == null ? getDefaultLobLength() : length + ); + default: + return super.resolveSize( jdbcType, javaType, precision, scale, length ); + } + } + }; + + @Override + protected DatabaseVersion getMinimumSupportedVersion() { + return MINIMUM_VERSION; + } + + @Override + public boolean useMaterializedLobWhenCapacityExceeded() { + return false; + } + + @Override + public String extractPattern(TemporalUnit unit) { + switch ( unit ) { + case SECOND: + return "(second(?2)+microsecond(?2)/1e6)"; + case WEEK: + return "weekofyear(?2)"; + case DAY_OF_WEEK: + return "dayofweek(?2)"; + case DAY_OF_MONTH: + return "dayofmonth(?2)"; + case DAY_OF_YEAR: + return "dayofyear(?2)"; + case EPOCH: + return "unix_timestamp(?2)"; + default: + return "?1(?2)"; + } + } + + @Override + public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { + if ( temporalType == TemporalType.TIME ) { + switch ( unit ) { + case NANOSECOND: + return "time(timestampadd(microsecond,(?2)/1e3,to_timestamp(?3, 'HH24:MI:SS.FF6')))"; + case NATIVE: + return "time(timestampadd(microsecond, ?2, to_timestamp(?3, 'HH24:MI:SS.FF6')))"; + case SECOND: + return "time(timestampadd(microsecond, ?2 * 1000000, to_timestamp(?3, 'HH24:MI:SS.FF6')))"; // to handle seconds fraction part + default: + return "time(timestampadd(?1, ?2, to_timestamp(?3, 'HH24:MI:SS.FF6')))"; + } + } + switch ( unit ) { + case NANOSECOND: + return "timestampadd(microsecond,(?2)/1e3,?3)"; + case NATIVE: + return "timestampadd(microsecond,?2,?3)"; + case SECOND: + return "timestampadd(microsecond,?2 * 1000000,?3)"; // to handle seconds fraction part + default: + return "timestampadd(?1,?2,?3)"; + } + } + + @Override + public String timestampdiffPattern(TemporalUnit unit, TemporalType fromTemporalType, TemporalType toTemporalType) { + String fromType = fromTemporalType == TemporalType.TIME ? "to_timestamp(?2, 'HH24:MI:SS.FF6')" : "?2"; + String toType = toTemporalType == TemporalType.TIME ? "to_timestamp(?3, 'HH24:MI:SS.FF6')" : "?3"; + switch ( unit ) { + case NANOSECOND: + return String.format( "timestampdiff(microsecond,%s,%s)*1e3", fromType, toType ); + case NATIVE: + return String.format( "timestampdiff(microsecond,%s,%s)", fromType, toType ); + default: + return String.format( "timestampdiff(?1,%s,%s)", fromType, toType ); + } + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, TemporalAccessor temporalAccessor, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date('" ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( "')" ); + break; + case TIME: + appender.appendSql( "time('" ); + appendAsLocalTime( appender, temporalAccessor ); + appender.appendSql( "')" ); + break; + case TIMESTAMP: + if ( temporalAccessor instanceof ZonedDateTime ) { + temporalAccessor = ( (ZonedDateTime) temporalAccessor ).toOffsetDateTime(); + } + appender.appendSql( "timestamp('" ); + appendAsTimestampWithMicros( + appender, + temporalAccessor, + supportsTemporalLiteralOffset(), + jdbcTimeZone, + false + ); + appender.appendSql( "')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date('" ); + appendAsDate( appender, date ); + appender.appendSql( "')" ); + break; + case TIME: + appender.appendSql( "time('" ); + appendAsLocalTime( appender, date ); + appender.appendSql( "')" ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp('" ); + appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appender.appendSql( "')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral( + SqlAppender appender, Calendar calendar, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( "date('" ); + appendAsDate( appender, calendar ); + appender.appendSql( "')" ); + break; + case TIME: + appender.appendSql( "time('" ); + appendAsLocalTime( appender, calendar ); + appender.appendSql( "')" ); + break; + case TIMESTAMP: + appender.appendSql( "timestamp('" ); + appendAsTimestampWithMillis( appender, calendar, jdbcTimeZone ); + appender.appendSql( "')" ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public SelectItemReferenceStrategy getGroupBySelectItemReferenceStrategy() { + return SelectItemReferenceStrategy.POSITION; + } + + //Creating an index on an ENUM column on columnstore tables is not supported. + @Override + public String getEnumTypeDeclaration(String name, String[] values) { + StringBuilder type = new StringBuilder(); + type.append( "enum (" ); + String separator = ""; + for ( String value : values ) { + type.append( separator ).append( '\'' ).append( value ).append( '\'' ); + separator = ","; + } + return type.append( ')' ).toString(); + } + + @Override + public String getQueryHintString(String query, String hints) { + return IndexQueryHintHandler.INSTANCE.addQueryHints( query, hints ); + } + + @Override + public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { + return EXTRACTOR; + } + + private static final ViolatedConstraintNameExtractor EXTRACTOR = new TemplatedViolatedConstraintNameExtractor( sqle -> { + final String sqlState = JdbcExceptionHelper.extractSqlState( sqle ); + if ( sqlState != null ) { + if ( Integer.parseInt( sqlState ) == 23000 ) { + return extractUsingTemplate( " for key '", "'", sqle.getMessage() ); + } + } + return null; + } ); + + @Override + public boolean qualifyIndexName() { + return false; + } + + @Override + protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.registerColumnTypes( typeContributions, serviceRegistry ); + final DdlTypeRegistry ddlTypeRegistry = typeContributions.getTypeConfiguration().getDdlTypeRegistry(); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "json", this ) ); + + final int maxTinyLobLen = 225; + final int maxLobLen = 65_535; + final int maxMediumLobLen = 16_777_215; + + final CapacityDependentDdlType.Builder varcharBuilder = CapacityDependentDdlType.builder( + VARCHAR, + CapacityDependentDdlType.LobKind.BIGGEST_LOB, + columnType( CLOB ), + columnType( CHAR ), + castType( CHAR ), + this + ).withTypeCapacity( getMaxVarcharLength(), "varchar($l)" ).withTypeCapacity( maxMediumLobLen, "mediumtext" ); + if ( getMaxVarcharLength() < maxLobLen ) { + varcharBuilder.withTypeCapacity( maxLobLen, "text" ); + } + ddlTypeRegistry.addDescriptor( varcharBuilder.build() ); + + // SingleStore doesn't support nchar/nvarchar/ntext + final CapacityDependentDdlType.Builder nvarcharBuilder = CapacityDependentDdlType.builder( + NVARCHAR, + CapacityDependentDdlType.LobKind.BIGGEST_LOB, + columnType( NCLOB ), + columnType( NCHAR ), + castType( NCHAR ), + this + ).withTypeCapacity( getMaxVarcharLength(), "varchar($l) character set utf8" ).withTypeCapacity( + maxMediumLobLen, + "mediumtext character set utf8" + ); + if ( getMaxVarcharLength() < maxLobLen ) { + nvarcharBuilder.withTypeCapacity( maxLobLen, "text character set utf8" ); + } + ddlTypeRegistry.addDescriptor( nvarcharBuilder.build() ); + + final CapacityDependentDdlType.Builder varbinaryBuilder = CapacityDependentDdlType.builder( + VARBINARY, + CapacityDependentDdlType.LobKind.BIGGEST_LOB, + columnType( BLOB ), + columnType( BINARY ), + castType( BINARY ), + this + ).withTypeCapacity( getMaxVarbinaryLength(), "varbinary($l)" ).withTypeCapacity( + maxMediumLobLen, + "mediumblob" + ); + if ( getMaxVarbinaryLength() < maxLobLen ) { + varbinaryBuilder.withTypeCapacity( maxLobLen, "blob" ); + } + ddlTypeRegistry.addDescriptor( varbinaryBuilder.build() ); + + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( + LONG32VARBINARY, + columnType( BLOB ), + castType( BINARY ), + this + ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32VARCHAR, columnType( CLOB ), castType( CHAR ), this ) ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( LONG32NVARCHAR, columnType( CLOB ), castType( CHAR ), this ) ); + + ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( + BLOB, + columnType( BLOB ), + castType( BINARY ), + this + ) + .withTypeCapacity( maxTinyLobLen, "tinyblob" ) + .withTypeCapacity( maxMediumLobLen, "mediumblob" ) + .withTypeCapacity( maxLobLen, "blob" ) + .build() ); + + ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( + CLOB, + columnType( CLOB ), + castType( CHAR ), + this + ) + .withTypeCapacity( maxTinyLobLen, "tinytext" ) + .withTypeCapacity( maxMediumLobLen, "mediumtext" ) + .withTypeCapacity( maxLobLen, "text" ) + .build() ); + + ddlTypeRegistry.addDescriptor( CapacityDependentDdlType.builder( + NCLOB, + columnType( NCLOB ), + castType( NCHAR ), + this + ).withTypeCapacity( maxTinyLobLen, "tinytext character set utf8" ).withTypeCapacity( + maxMediumLobLen, + "mediumtext character set utf8" + ).withTypeCapacity( maxLobLen, "text character set utf8" ).build() ); + + ddlTypeRegistry.addDescriptor( new NativeEnumDdlTypeImpl( this ) ); + ddlTypeRegistry.addDescriptor( new NativeOrdinalEnumDdlTypeImpl( this ) ); + } + + @Override + public void initializeFunctionRegistry(FunctionContributions functionContributions) { + super.initializeFunctionRegistry( functionContributions ); + CommonFunctionFactory commonFunctionFactory = new CommonFunctionFactory( functionContributions ); + commonFunctionFactory.windowFunctions(); + commonFunctionFactory.radians(); + commonFunctionFactory.degrees(); + commonFunctionFactory.cot(); + commonFunctionFactory.log(); + commonFunctionFactory.log2(); + commonFunctionFactory.log10(); + commonFunctionFactory.trim2(); + commonFunctionFactory.octetLength(); + commonFunctionFactory.reverse(); + commonFunctionFactory.pad_space(); + commonFunctionFactory.md5(); + commonFunctionFactory.yearMonthDay(); + commonFunctionFactory.hourMinuteSecond(); + commonFunctionFactory.dayofweekmonthyear(); + commonFunctionFactory.weekQuarter(); + commonFunctionFactory.daynameMonthname(); + commonFunctionFactory.lastDay(); + commonFunctionFactory.date(); + commonFunctionFactory.timestamp(); + commonFunctionFactory.utcDateTimeTimestamp(); + commonFunctionFactory.rand(); + commonFunctionFactory.crc32(); + commonFunctionFactory.sha1(); + commonFunctionFactory.sha2(); + commonFunctionFactory.sha(); + commonFunctionFactory.octetLength(); + commonFunctionFactory.ascii(); + commonFunctionFactory.instr(); + commonFunctionFactory.substr(); + commonFunctionFactory.position(); + commonFunctionFactory.nowCurdateCurtime(); + commonFunctionFactory.trunc_truncate(); + commonFunctionFactory.bitandorxornot_operator(); + commonFunctionFactory.bitAndOr(); + commonFunctionFactory.stddev(); + commonFunctionFactory.stddevPopSamp(); + commonFunctionFactory.variance(); + commonFunctionFactory.varPopSamp(); + commonFunctionFactory.datediff(); + commonFunctionFactory.adddateSubdateAddtimeSubtime(); + commonFunctionFactory.format_dateFormat(); + commonFunctionFactory.makedateMaketime(); + commonFunctionFactory.localtimeLocaltimestamp(); + commonFunctionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); + commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation(); + commonFunctionFactory.listagg_groupConcat(); + functionContributions.getFunctionRegistry() + .namedDescriptorBuilder( "time" ) + .setExactArgumentCount( 1 ) + .setInvariantType( functionContributions.getTypeConfiguration() + .getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ) ) + .register(); + functionContributions.getFunctionRegistry() + .patternDescriptorBuilder( "median", "median(?1) over ()" ) + .setInvariantType( functionContributions.getTypeConfiguration() + .getBasicTypeRegistry() + .resolve( StandardBasicTypes.DOUBLE ) ) + .setExactArgumentCount( 1 ) + .setParameterTypes( NUMERIC ) + .register(); + BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); + SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); + functionRegistry.noArgsBuilder( "localtime" ) + .setInvariantType( basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ) ) + .setUseParenthesesWhenNoArgs( false ) + .register(); + functionRegistry.patternDescriptorBuilder( "pi", "pi() :> double" ).setInvariantType( basicTypeRegistry.resolve( + StandardBasicTypes.DOUBLE ) ).setExactArgumentCount( 0 ).setArgumentListSignature( "" ).register(); + functionRegistry.patternDescriptorBuilder( "chr", "char(?1 using utf8mb4)" ) + .setInvariantType( basicTypeRegistry.resolve( StandardBasicTypes.CHARACTER ) ) + .setExactArgumentCount( 1 ) + .setParameterTypes( FunctionParameterType.INTEGER ) + .register(); + functionRegistry.registerAlternateKey( "char", "chr" ); + } + + + @Override + public String getCreateTableString() { + return explicitTableType == null ? "create table" : String.format( + "create %s table", + explicitTableType.name().toLowerCase() + ); + } + + @Override + public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + super.contributeTypes( typeContributions, serviceRegistry ); + + final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); + + typeContributions.contributeJdbcType( NullJdbcType.INSTANCE ); + + // Until we remove StandardBasicTypes, we have to keep this + typeContributions.contributeType( new NullType( + NullJdbcType.INSTANCE, + typeContributions.getTypeConfiguration() + .getJavaTypeRegistry() + .getDescriptor( Object.class ) + ) ); + + jdbcTypeRegistry.addDescriptor( EnumJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( OrdinalEnumJdbcType.INSTANCE ); + } + + @Override + public JdbcType resolveSqlTypeDescriptor( + String columnTypeName, int jdbcTypeCode, int precision, int scale, JdbcTypeRegistry jdbcTypeRegistry) { + switch ( jdbcTypeCode ) { + case Types.BIT: + return jdbcTypeRegistry.getDescriptor( Types.TINYINT ); + case Types.OTHER: + if ( "GEOGRAPHY".equals( columnTypeName ) || "GEOGRAPHYPOINT".equals( columnTypeName ) ) { + jdbcTypeCode = VARCHAR; + } + break; + } + return super.resolveSqlTypeDescriptor( columnTypeName, jdbcTypeCode, precision, scale, jdbcTypeRegistry ); + } + + @Override + protected String columnType(int sqlTypeCode) { + switch ( sqlTypeCode ) { + case BOOLEAN: + return "bit"; + case TIMESTAMP: + return "datetime($p)"; + case TIMESTAMP_WITH_TIMEZONE: + return "timestamp($p)"; + case TIME_WITH_TIMEZONE: + return "time($p)"; + case SqlTypes.NUMERIC: + return columnType( DECIMAL ); + case FLOAT: + // Avoid using float type because + // SingleStore has potential inaccuracy when using the = or != comparison operators on FLOAT columns in WHERE clause + return columnType( DOUBLE ); + case NCHAR: + return "char($l) character set utf8"; + case NVARCHAR: + return "varchar($l) character set utf8"; + case BLOB: + return "longblob"; + case NCLOB: + return "longtext character set utf8"; + case CLOB: + return "longtext"; + default: + return super.columnType( sqlTypeCode ); + } + } + + @Override + public String castPattern(CastType from, CastType to) { + if ( CastType.FLOAT == to || CastType.DOUBLE == to || CastType.OTHER == to ) { + return "?1 :> ?2"; + } + return super.castPattern( from, to ); + } + + @Override + public int getPreferredSqlTypeCodeForBoolean() { + return Types.BIT; + } + + @Override + protected String castType(int sqlTypeCode) { + switch ( sqlTypeCode ) { + case BOOLEAN: + case BIT: + //special case for casting to Boolean + return "unsigned"; + case TINYINT: + case SMALLINT: + case INTEGER: + case BIGINT: + return "signed"; + case CHAR: + case VARCHAR: + case LONG32VARCHAR: + return "char"; + case NCHAR: + case NVARCHAR: + case LONG32NVARCHAR: + return "char character set utf8"; + case BINARY: + case VARBINARY: + case LONG32VARBINARY: + return "binary"; + } + return super.castType( sqlTypeCode ); + } + + @Override + public SizeStrategy getSizeStrategy() { + return sizeStrategy; + } + + @Override + public int getFloatPrecision() { + //the maximum precision for 4 bytes + return 23; + } + + @Override + public String currentTimestamp() { + return "current_timestamp(6)"; + } + + @Override + public long getFractionalSecondPrecisionInNanos() { + return 1_000; //microseconds + } + + @Override + public long getDefaultLobLength() { + return Length.LONG32; + } + + @Override + public int resolveSqlTypeLength( + String columnTypeName, int jdbcTypeCode, int precision, int scale, int displaySize) { + if ( jdbcTypeCode == Types.CHAR && precision <= 4 ) { + return displaySize; + } + else { + return precision; + } + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new SingleStoreSqlAstTranslator<>( sessionFactory, statement ); + } + }; + } + + @Override + public SchemaNameResolver getSchemaNameResolver() { + return (connection, dialect) -> ""; + } + + @Override + public int getInExpressionCountLimit() { + return PARAM_LIST_SIZE_LIMIT; + } + + /** + * The biggest size value that can be supplied as argument + */ + @Override + public int getMaxVarbinaryLength() { + return 65_533; + } + + @Override + public int getMaxVarcharLength() { + return 21_844; + } + + @Override + public String getNullColumnString(String columnType) { + if ( columnType.regionMatches( true, 0, "timestamp", 0, "timestamp".length() ) ) { + return " null"; + } + return super.getNullColumnString( columnType ); + } + + /** + * Feature 'Check constraints' is not supported by SingleStore. + */ + @Override + public boolean supportsColumnCheck() { + return false; + } + + /** + * Feature 'Check constraints' is not supported by SingleStore. + */ + public boolean supportsTableCheck() { + return false; + } + + @Override + public int getDefaultDecimalPrecision() { + return 65; + } + + @Override + public boolean doesRoundTemporalOnOverflow() { + return false; + } + + @Override + public boolean supportsWindowFunctions() { + return true; + } + + @Override + public boolean supportsRecursiveCTE() { + return true; + } + + @Override + public boolean dropConstraints() { + return false; + } + + @Override + public void appendLiteral(SqlAppender appender, String literal) { + appender.appendSql( '\'' ); + for ( int i = 0; i < literal.length(); i++ ) { + final char c = literal.charAt( i ); + if ( c == '\'' ) { + appender.appendSql( '\'' ); + } + else if ( c == '\\' ) { + appender.appendSql( '\\' ); + } + appender.appendSql( c ); + } + appender.appendSql( '\'' ); + } + + @Override + public void appendDatetimeFormat(SqlAppender appender, String format) { + appender.appendSql( datetimeFormat( format ).result() ); + } + + public static Replacer datetimeFormat(String format) { + return new Replacer( format, "'", "" ).replace( "%", "%%" ) + + //year + .replace( "yyyy", "%Y" ) + .replace( "yyy", "%Y" ) + .replace( "yy", "%y" ) + .replace( "y", "%Y" ) + + //month of year + .replace( "MMMM", "%M" ) + .replace( "MMM", "%b" ) + .replace( "MM", "%m" ) + .replace( "M", "%c" ) + + //week of year + .replace( "ww", "%v" ) + .replace( "w", "%v" ) + //year for week + .replace( "YYYY", "%x" ) + .replace( "YYY", "%x" ) + .replace( "YY", "%x" ) + .replace( "Y", "%x" ) + + //week of month + //???? + + //day of week + .replace( "EEEE", "%W" ) + .replace( "EEE", "%a" ) + .replace( "ee", "%w" ) + .replace( "e", "%w" ) + + //day of month + .replace( "dd", "%d" ) + .replace( "d", "%e" ) + + //day of year + .replace( "DDD", "%j" ) + .replace( "DD", "%j" ) + .replace( "D", "%j" ) + + //am pm + .replace( "a", "%p" ) + + //hour + .replace( "hh", "%I" ) + .replace( "HH", "%H" ) + .replace( "h", "%l" ) + .replace( "H", "%k" ) + + //minute + .replace( "mm", "%i" ) + .replace( "m", "%i" ) + + //second + .replace( "ss", "%S" ) + .replace( "s", "%S" ) + + //fractional seconds + .replace( "SSSSSS", "%f" ) + .replace( "SSSSS", "%f" ) + .replace( "SSSS", "%f" ) + .replace( "SSS", "%f" ) + .replace( "SS", "%f" ) + .replace( "S", "%f" ); + } + + @Override + public String getDropForeignKeyString() { + throw new UnsupportedOperationException( "SingleStore does not support foreign keys and referential integrity" ); + } + + @Override + public String getDropUniqueKeyString() { + return "drop index"; + } + + @Override + public String getAlterColumnTypeString(String columnName, String columnType, String columnDefinition) { + // no way to change just the column type, leaving other attributes intact + return "modify column " + columnName + " " + columnDefinition.trim(); + } + + /** + * SingleStore doesn't support modifying column type on columnstore tables. + * It only supports modifying column type on rowstore table. + */ + @Override + public boolean supportsAlterColumnType() { + return false; + } + + @Override + public LimitHandler getLimitHandler() { + //also supports LIMIT n OFFSET m + return LimitLimitHandler.INSTANCE; + } + + @Override + public char closeQuote() { + return '`'; + } + + @Override + public char openQuote() { + return '`'; + } + + @Override + public boolean canCreateCatalog() { + return true; + } + + @Override + public String[] getCreateCatalogCommand(String catalogName) { + return new String[] { "create database " + catalogName }; + } + + @Override + public String[] getDropCatalogCommand(String catalogName) { + return new String[] { "drop database " + catalogName }; + } + + @Override + public boolean canCreateSchema() { + return false; + } + + @Override + public String[] getCreateSchemaCommand(String schemaName) { + throw new UnsupportedOperationException( + "SingleStore does not support dropping creating/dropping schemas in the JDBC sense" ); + } + + @Override + public String[] getDropSchemaCommand(String schemaName) { + throw new UnsupportedOperationException( + "SingleStore does not support dropping creating/dropping schemas in the JDBC sense" ); + } + + @Override + public boolean supportsIfExistsBeforeTableName() { + return true; + } + + @Override + public String getSelectGUIDString() { + return "select uuid()"; + } + + @Override + public boolean supportsCommentOn() { + return true; + } + + @Override + public String getTableComment(String comment) { + return " comment='" + comment + "'"; + } + + @Override + public String getColumnComment(String comment) { + return " comment '" + comment + "'"; + } + + @Override + public NullOrdering getNullOrdering() { + return NullOrdering.SMALLEST; + } + + @Override + public SqmMultiTableMutationStrategy getFallbackSqmMutationStrategy( + EntityMappingType rootEntityDescriptor, RuntimeModelCreationContext runtimeModelCreationContext) { + + return new LocalTemporaryTableMutationStrategy( TemporaryTable.createIdTable( + rootEntityDescriptor, + basename -> TemporaryTable.ID_TABLE_PREFIX + basename, + this, + runtimeModelCreationContext + ), runtimeModelCreationContext.getSessionFactory() ); + } + + @Override + public SqmMultiTableInsertStrategy getFallbackSqmInsertStrategy( + EntityMappingType rootEntityDescriptor, RuntimeModelCreationContext runtimeModelCreationContext) { + + return new LocalTemporaryTableInsertStrategy( TemporaryTable.createEntityTable( + rootEntityDescriptor, + name -> TemporaryTable.ENTITY_TABLE_PREFIX + name, + this, + runtimeModelCreationContext + ), runtimeModelCreationContext.getSessionFactory() ); + } + + @Override + public TemporaryTableKind getSupportedTemporaryTableKind() { + return TemporaryTableKind.LOCAL; + } + + @Override + public String getTemporaryTableCreateCommand() { + return "create temporary table if not exists"; + } + + //SingleStore throws an error on drop temporary table if there are uncommited statements within transaction. + //Just 'drop table' statement causes implicit commit, so using 'delete from'. + @Override + public String getTemporaryTableDropCommand() { + return "delete from"; + } + + @Override + public AfterUseAction getTemporaryTableAfterUseAction() { + return AfterUseAction.DROP; + } + + @Override + public BeforeUseAction getTemporaryTableBeforeUseAction() { + return BeforeUseAction.CREATE; + } + + @Override + public int getMaxAliasLength() { + return 64; + } + + @Override + public int getMaxIdentifierLength() { + return 64; + } + + @Override + public boolean supportsIsTrue() { + return true; + } + + @Override + public boolean supportsCurrentTimestampSelection() { + return true; + } + + @Override + public boolean isCurrentTimestampSelectStringCallable() { + return false; + } + + @Override + public String getCurrentTimestampSelectString() { + return "select now()"; + } + + @Override + public int registerResultSetOutParameter(CallableStatement statement, int col) throws SQLException { + throw new UnsupportedOperationException( "SingleStore does not support resultsets via stored procedures." ); + } + + @Override + public ResultSet getResultSet(CallableStatement ps) throws SQLException { + boolean isResultSet = ps.execute(); + while ( !isResultSet && ps.getUpdateCount() != -1 ) { + isResultSet = ps.getMoreResults(); + } + return ps.getResultSet(); + } + + @Override + public boolean supportsNullPrecedence() { + return false; + } + + @Override + public boolean supportsLobValueChangePropagation() { + return false; + } + + @Override + public boolean supportsSubqueryOnMutatingTable() { + return false; + } + + @Override + public boolean supportsLockTimeouts() { + return false; + } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + switch ( sqlException.getErrorCode() ) { + case 1205: + case 3572: + return new PessimisticLockException( message, sqlException, sql ); + case 1207: + case 1206: + return new LockAcquisitionException( message, sqlException, sql ); + case 1062: + String constraintName = getViolatedConstraintNameExtractor().extractConstraintName( sqlException ); + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + constraintName + ); + } + + final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "41000": + return new LockTimeoutException( message, sqlException, sql ); + case "40001": + return new LockAcquisitionException( message, sqlException, sql ); + } + } + + return null; + }; + } + + @Override + public NameQualifierSupport getNameQualifierSupport() { + return NameQualifierSupport.CATALOG; + } + + @Override + public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) + throws SQLException { + builder.setUnquotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + builder.setQuotedCaseStrategy( IdentifierCaseStrategy.MIXED ); + return super.buildIdentifierHelper( builder, dbMetaData ); + } + + @Override + public String getAddForeignKeyConstraintString( + String constraintName, + String[] foreignKey, + String referencedTable, + String[] primaryKey, + boolean referencesPrimaryKey) { + throw new UnsupportedOperationException( "SingleStore does not support foreign keys and referential integrity." ); + } + + @Override + public String getAddForeignKeyConstraintString( + String constraintName, String foreignKeyDefinition) { + throw new UnsupportedOperationException( "SingleStore does not support foreign keys and referential integrity." ); + } + + @Override + public String getAddPrimaryKeyConstraintString(String constraintName) { + throw new UnsupportedOperationException( "SingleStore does not support altering primary key." ); + } + + @Override + public String getWriteLockString(String aliases, int timeout) { + return getForUpdateString( aliases ); + } + + @Override + public String getForUpdateSkipLockedString(String aliases) { + return getForUpdateString(); + } + + @Override + public String getForUpdateNowaitString(String aliases) { + return getForUpdateString(); + } + + + @Override + public String getForUpdateString() { + return isForUpdateLockingEnabled ? super.getForUpdateString() : ""; + } + + @Override + public boolean supportsOffsetInSubquery() { + return true; + } + + @Override + public boolean supportsPartitionBy() { + return true; + } + + @Override + public boolean supportsWait() { + return false; + } + + @Override + protected void registerDefaultKeywords() { + super.registerDefaultKeywords(); + registerKeyword( "key" ); + } + + @Override + public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSupport() { + return FunctionalDependencyAnalysisSupportImpl.TABLE_GROUP; + } + + @Override + public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { + return DmlTargetColumnQualifierSupport.TABLE_ALIAS; + } + + @Override + public boolean supportsFromClauseInUpdate() { + return true; + } + + @Override + public boolean supportsCircularCascadeDeleteConstraints() { + return false; + } + + @Override + public IdentityColumnSupport getIdentityColumnSupport() { + return MySQLIdentityColumnSupport.INSTANCE; + } + + @Override + public boolean isJdbcLogWarningsEnabledByDefault() { + return false; + } + + @Override + public boolean supportsCascadeDelete() { + return false; + } + + @Override + public boolean supportsNonQueryWithCTE() { + return true; + } + + @Override + public Exporter getSequenceExporter() { + return NOOP_EXPORTER; + } + + /** + * SingleStore does not support foreign keys and referential integrity + */ + @Override + public Exporter getForeignKeyExporter() { + return NOOP_EXPORTER; + } + + @Override + public UniqueDelegate getUniqueDelegate() { + return NOOP_UNIQUE_DELEGATE; + } + + /** + * A no-op {@link Exporter} which is responsible for returning empty Create and Drop SQL strings. + */ + static class EmptyExporter implements Exporter { + + @Override + public String[] getSqlCreateStrings(T exportable, Metadata metadata, SqlStringGenerationContext context) { + return ArrayHelper.EMPTY_STRING_ARRAY; + } + + @Override + public String[] getSqlDropStrings(T exportable, Metadata metadata, SqlStringGenerationContext context) { + return ArrayHelper.EMPTY_STRING_ARRAY; + } + } + + /** + * Because of hibernate requires that entity tables have primary key separate unique keys are restricted. + * SingleStore restrictions: + * - Primary key in SingleStore table is unique key and shard key + * - SingleStore table allows only single shard key + * - SingleStore unique keys must contain all columns of the shard key: Unique Key restrictions. + * - Shard key fields cannot be updated (or altered) so they must be fields that never change + */ + static class DoNothingUniqueDelegate implements UniqueDelegate { + + @Override + public String getColumnDefinitionUniquenessFragment(Column column, SqlStringGenerationContext context) { + return ""; + } + + @Override + public String getTableCreationUniqueConstraintsFragment(Table table, SqlStringGenerationContext context) { + return ""; + } + + @Override + public String getAlterTableToAddUniqueKeyCommand( + UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { + return ""; + } + + @Override + public String getAlterTableToDropUniqueKeyCommand( + UniqueKey uniqueKey, Metadata metadata, SqlStringGenerationContext context) { + return ""; + } + } + + /** + * The default table type in SingleStore is 'columnstore'. The default can be changed to 'rowstore' by updating the + * 'default_table_type' engine variable to 'rowstore' or specify explicitly by property : 'hibernate.dialect.singlestore.table_type'. + * Choosing a Table Storage Type + */ + public enum SingleStoreTableType { + COLUMNSTORE, ROWSTORE; + + public static SingleStoreTableType fromValue(String value) { + return Arrays.stream( values() ) + .filter( v -> v.name().equalsIgnoreCase( value.trim() ) ) + .findAny() + .orElseThrow( () -> new IllegalArgumentException( "Wrong table type" ) ); + } + } + + public SingleStoreTableType getExplicitTableType() { + return explicitTableType; + } + + public boolean isForUpdateLockingEnabled() { + return isForUpdateLockingEnabled; + } + + /** + * Specifies SingleStore explicit table type. + * + * @settingDefault {@code null} + */ + public static final String SINGLE_STORE_TABLE_TYPE = "hibernate.dialect.singlestore.table_type"; + /** + * Specifies SingleStore FOR UPDATE clause lock enable. + * + * @settingDefault {@code false} + */ + public static final String SINGLE_STORE_FOR_UPDATE_LOCK_ENABLED = "hibernate.dialect.singlestore.for_update_lock_enabled"; +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java new file mode 100644 index 0000000000..c646a55005 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java @@ -0,0 +1,464 @@ +/* + * 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.community.dialect; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.dialect.DialectDelegateWrapper; +import org.hibernate.dialect.DmlTargetColumnQualifierSupport; +import org.hibernate.dialect.MySQLSqlAstTranslator; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.query.sqm.ComparisonOperator; +import org.hibernate.query.sqm.SetOperator; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.delete.DeleteStatement; +import org.hibernate.sql.ast.tree.expression.Any; +import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; +import org.hibernate.sql.ast.tree.expression.CastTarget; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Every; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.Literal; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.Summarization; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.QueryPartTableReference; +import org.hibernate.sql.ast.tree.insert.ConflictClause; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.LikePredicate; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.JdbcOperation; + +/** + * A SQL AST translator for SingleStore. + * + * @author Oleksandr Yeliseiev + */ +public class SingleStoreSqlAstTranslator extends AbstractSqlAstTranslator { + + private final SingleStoreDialect dialect; + + public SingleStoreSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + this.dialect = (SingleStoreDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); + } + + @Override + public void visitBinaryArithmeticExpression(BinaryArithmeticExpression arithmeticExpression) { + if ( isIntegerDivisionEmulationRequired( arithmeticExpression ) ) { + appendSql( OPEN_PARENTHESIS ); + visitArithmeticOperand( arithmeticExpression.getLeftHandOperand() ); + appendSql( " div " ); + visitArithmeticOperand( arithmeticExpression.getRightHandOperand() ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.visitBinaryArithmeticExpression( arithmeticExpression ); + } + } + + @Override + protected boolean supportsRowValueConstructorSyntax() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInInList() { + return false; + } + + @Override + protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + return false; + } + + @Override + protected void renderSelectTupleComparison( + List lhsExpressions, SqlTuple tuple, ComparisonOperator operator) { + emulateSelectTupleComparison( lhsExpressions, tuple.getExpressions(), operator, true ); + } + + @Override + protected void visitInsertSource(InsertSelectStatement statement) { + if ( statement.getSourceSelectStatement() != null ) { + if ( statement.getConflictClause() != null ) { + final List targetColumnReferences = statement.getTargetColumns(); + final List columnNames = new ArrayList<>( targetColumnReferences.size() ); + for ( ColumnReference targetColumnReference : targetColumnReferences ) { + columnNames.add( targetColumnReference.getColumnExpression() ); + } + appendSql( "select * from " ); + emulateQueryPartTableReferenceColumnAliasing( new QueryPartTableReference( + new SelectStatement( statement.getSourceSelectStatement() ), + "excluded", + columnNames, + false, + getSessionFactory() + ) ); + } + else { + statement.getSourceSelectStatement().accept( this ); + } + } + else { + visitValuesList( statement.getValuesList() ); + } + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + final Statement currentStatement; + if ( "excluded".equals( columnReference.getQualifier() ) && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) { + // Accessing the excluded row for an insert-values statement in the conflict clause requires the values qualifier + appendSql( "values(" ); + columnReference.appendReadExpression( this, null ); + append( ')' ); + } + else { + super.visitColumnReference( columnReference ); + } + } + + @Override + protected void renderDeleteClause(DeleteStatement statement) { + appendSql( "delete" ); + final Stack clauseStack = getClauseStack(); + try { + clauseStack.push( Clause.DELETE ); + renderTableReferenceIdentificationVariable( statement.getTargetTable() ); + if ( statement.getFromClause().getRoots().isEmpty() ) { + appendSql( " from " ); + renderDmlTargetTableExpression( statement.getTargetTable() ); + } + else { + visitFromClause( statement.getFromClause() ); + } + } + finally { + clauseStack.pop(); + } + } + + @Override + protected void renderUpdateClause(UpdateStatement updateStatement) { + if ( updateStatement.getFromClause().getRoots().isEmpty() ) { + super.renderUpdateClause( updateStatement ); + } + else { + appendSql( "update " ); + renderFromClauseSpaces( updateStatement.getFromClause() ); + } + } + + @Override + protected void renderDmlTargetTableExpression(NamedTableReference tableReference) { + super.renderDmlTargetTableExpression( tableReference ); + if ( getClauseStack().getCurrent() != Clause.INSERT ) { + renderTableReferenceIdentificationVariable( tableReference ); + } + } + + @Override + protected boolean supportsJoinsInDelete() { + return true; + } + + @Override + protected void visitConflictClause(ConflictClause conflictClause) { + visitOnDuplicateKeyConflictClause( conflictClause ); + } + + @Override + protected String determineColumnReferenceQualifier(ColumnReference columnReference) { + final DmlTargetColumnQualifierSupport qualifierSupport = getDialect().getDmlTargetColumnQualifierSupport(); + final MutationStatement currentDmlStatement; + final String dmlAlias; + // Since SingleStore does not support aliasing the insert target table, + // we must detect column reference that are used in the conflict clause + // and use the table expression as qualifier instead + if ( getClauseStack().getCurrent() != Clause.SET || !( ( currentDmlStatement = getCurrentDmlStatement() ) instanceof InsertSelectStatement ) || ( dmlAlias = currentDmlStatement.getTargetTable() + .getIdentificationVariable() ) == null || !dmlAlias.equals( columnReference.getQualifier() ) ) { + return columnReference.getQualifier(); + } + // Qualify the column reference with the table expression also when in subqueries + else if ( qualifierSupport != DmlTargetColumnQualifierSupport.NONE || !getQueryPartStack().isEmpty() ) { + return getCurrentDmlStatement().getTargetTable().getTableExpression(); + } + else { + return null; + } + } + + @Override + protected boolean supportsWithClauseInSubquery() { + return false; + } + + @Override + protected void renderExpressionAsClauseItem(Expression expression) { + expression.accept( this ); + } + + + @Override + public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanExpressionPredicate) { + final boolean isNegated = booleanExpressionPredicate.isNegated(); + if ( isNegated ) { + appendSql( "not(" ); + } + booleanExpressionPredicate.getExpression().accept( this ); + if ( isNegated ) { + appendSql( CLOSE_PARENTHESIS ); + } + } + + protected boolean shouldEmulateFetchClause(QueryPart queryPart) { + // Check if current query part is already row numbering to avoid infinite recursion + return useOffsetFetchClause( queryPart ) && getQueryPartForRowNumbering() != queryPart && supportsWindowFunctions() && !isRowsOnlyFetchClauseType( + queryPart ); + } + + @Override + protected boolean shouldEmulateLateralWithIntersect(QueryPart queryPart) { + return supportsSimpleQueryGrouping() || !queryPart.hasOffsetOrFetchClause(); + } + + //SingleStore doesn't support 'FOR UPDATE' clause with distributed joins + @Override + protected String getForUpdate() { + return dialect.getForUpdateString(); + } + + @Override + public void visitAny(Any any) { + throw new UnsupportedOperationException( "SingleStore doesn't support ANY clause" ); + } + + @Override + public void visitEvery(Every every) { + throw new UnsupportedOperationException( "SingleStore doesn't support ALL clause" ); + } + + @Override + protected boolean supportsNestedSubqueryCorrelation() { + return false; + } + + @Override + public void visitQueryGroup(QueryGroup queryGroup) { + if ( shouldEmulateFetchClause( queryGroup ) ) { + emulateFetchOffsetWithWindowFunctions( queryGroup, true ); + } + else { + super.visitQueryGroup( queryGroup ); + } + } + + @Override + public void visitQuerySpec(QuerySpec querySpec) { + if ( shouldEmulateFetchClause( querySpec ) ) { + emulateFetchOffsetWithWindowFunctions( querySpec, true ); + } + else { + super.visitQuerySpec( querySpec ); + } + } + + @Override + public void visitQueryPartTableReference(QueryPartTableReference tableReference) { + emulateQueryPartTableReferenceColumnAliasing( tableReference ); + } + + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + if ( !isRowNumberingCurrentQueryPart() ) { + renderCombinedLimitClause( queryPart ); + } + } + + @Override + protected void renderComparison(Expression lhs, ComparisonOperator operator, Expression rhs) { + if ( operator == ComparisonOperator.DISTINCT_FROM || operator == ComparisonOperator.NOT_DISTINCT_FROM ) { + renderDistinct( lhs, operator, rhs ); + } + else { + lhs.accept( this ); + appendSql( operator.sqlText() ); + rhs.accept( this ); + } + } + + private void renderDistinct(Expression lhs, ComparisonOperator operator, Expression rhs) { + appendSql( OPEN_PARENTHESIS ); + appendSql( "case when " ); + rhs.accept( this ); + appendSql( " is null then " ); + if ( operator == ComparisonOperator.DISTINCT_FROM ) { + appendSql( OPEN_PARENTHESIS ); + lhs.accept( this ); + appendSql( " is not null) else (" ); + lhs.accept( this ); + appendSql( "!=" ); + rhs.accept( this ); + appendSql( " or " ); + lhs.accept( this ); + appendSql( " is null) end)" ); + } + else { + appendSql( OPEN_PARENTHESIS ); + lhs.accept( this ); + appendSql( " is null) else (" ); + lhs.accept( this ); + appendSql( "=" ); + rhs.accept( this ); + appendSql( ") end)" ); + } + } + + @Override + protected void emulateTupleComparison( + final List lhsExpressions, + final List rhsExpressions, + ComparisonOperator operator, + boolean indexOptimized) { + if ( operator == ComparisonOperator.DISTINCT_FROM || operator == ComparisonOperator.NOT_DISTINCT_FROM ) { + final int size = lhsExpressions.size(); + assert size == rhsExpressions.size(); + String separator = OPEN_PARENTHESIS + ""; + for ( int i = 0; i < size; i++ ) { + appendSql( separator ); + renderDistinct( (Expression) lhsExpressions.get( i ), operator, (Expression) rhsExpressions.get( i ) ); + separator = ") and ("; + } + appendSql( CLOSE_PARENTHESIS ); + } + else { + super.emulateTupleComparison( lhsExpressions, rhsExpressions, operator, indexOptimized ); + } + } + + @Override + protected void renderCombinedLimitClause(Expression offsetExpression, Expression fetchExpression) { + if ( offsetExpression != null || fetchExpression != null ) { + if ( getCurrentQueryPart() instanceof QueryGroup && ( ( (QueryGroup) getCurrentQueryPart() ).getSetOperator() == SetOperator.UNION || ( (QueryGroup) getCurrentQueryPart() ).getSetOperator() == SetOperator.UNION_ALL ) ) { + throw new UnsupportedOperationException( "SingleStore doesn't support UNION/UNION ALL with limit clause" ); + } + } + super.renderCombinedLimitClause( offsetExpression, fetchExpression ); + } + + + @Override + protected void renderPartitionItem(Expression expression) { + if ( expression instanceof Literal ) { + appendSql( "'0'" ); + } + else if ( expression instanceof Summarization ) { + Summarization summarization = (Summarization) expression; + renderCommaSeparated( summarization.getGroupings() ); + appendSql( " with " ); + appendSql( summarization.getKind().sqlText() ); + } + else { + expression.accept( this ); + } + } + + //SingleStore like is case insensitive + @Override + public void visitLikePredicate(LikePredicate likePredicate) { + appendSql( "cast( " ); + likePredicate.getMatchExpression().accept( this ); + appendSql( " as char) " ); + if ( likePredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " like " ); + renderBackslashEscapedLikePattern( likePredicate.getPattern(), likePredicate.getEscapeCharacter(), false ); + } + + @Override + protected void renderBackslashEscapedLikePattern( + Expression pattern, Expression escapeCharacter, boolean noBackslashEscapes) { + if ( escapeCharacter != null ) { + appendSql( "replace" ); + appendSql( OPEN_PARENTHESIS ); + pattern.accept( this ); + appendSql( "," ); + escapeCharacter.accept( this ); + appendSql( ",'\\\\'" ); + appendSql( CLOSE_PARENTHESIS ); + } + else { + // Since escape with empty or null character is ignored we need + // four backslashes to render a single one in a like pattern + if ( pattern instanceof Literal ) { + Object literalValue = ( (Literal) pattern ).getLiteralValue(); + if ( literalValue == null ) { + pattern.accept( this ); + } + else { + appendBackslashEscapedLikeLiteral( this, literalValue.toString(), false ); + } + } + else { + appendSql( "replace" ); + appendSql( OPEN_PARENTHESIS ); + pattern.accept( this ); + appendSql( ",'\\\\','\\\\\\\\'" ); + appendSql( CLOSE_PARENTHESIS ); + } + } + } + + @Override + public boolean supportsRowValueConstructorSyntaxInSet() { + return false; + } + + @Override + protected boolean supportsDistinctFromPredicate() { + return false; + } + + @Override + protected String getDual() { + return "dual"; + } + + @Override + public SingleStoreDialect getDialect() { + return this.dialect; + } + + private boolean supportsWindowFunctions() { + return true; + } + + @Override + public void visitCastTarget(CastTarget castTarget) { + String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() ); + if ( sqlType != null ) { + appendSql( sqlType ); + } + else { + super.visitCastTarget( castTarget ); + } + } +} diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/CommunityDialectSelectorTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/CommunityDialectSelectorTest.java index ed9db937ad..cdedcacff4 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/CommunityDialectSelectorTest.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/CommunityDialectSelectorTest.java @@ -72,6 +72,7 @@ public class CommunityDialectSelectorTest { testDialectNamingResolution( TeradataDialect.class ); testDialectNamingResolution( TimesTenDialect.class ); + testDialectNamingResolution( SingleStoreDialect.class ); } private void testDialectNamingResolution(final Class dialectClass) { diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SingleStoreDialectTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SingleStoreDialectTest.java new file mode 100644 index 0000000000..a25f9ee946 --- /dev/null +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/SingleStoreDialectTest.java @@ -0,0 +1,51 @@ +/* + * 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.community.dialect; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.hibernate.dialect.Dialect; +import org.hibernate.orm.test.dialect.resolver.TestingDialectResolutionInfo; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.dialect.DatabaseVersion.NO_VERSION; + +public class SingleStoreDialectTest { + + @Test + public void testSpecificProperties() { + final Dialect dialect = resolveDialect( "SingleStore", values -> { + values.put( SingleStoreDialect.SINGLE_STORE_TABLE_TYPE, "rowStOre " ); + values.put( SingleStoreDialect.SINGLE_STORE_FOR_UPDATE_LOCK_ENABLED, "true" ); + } ); + + assertThat( dialect ).isInstanceOf( SingleStoreDialect.class ); + SingleStoreDialect singleStoreDialect = (SingleStoreDialect) dialect; + assertThat( singleStoreDialect.getExplicitTableType() ).isEqualTo( SingleStoreDialect.SingleStoreTableType.ROWSTORE ); + assertThat( singleStoreDialect.isForUpdateLockingEnabled() ).isTrue(); + } + + private static Dialect resolveDialect(String productName, Consumer> configurationProvider) { + final Map configurationValues = new HashMap<>(); + configurationProvider.accept( configurationValues ); + final TestingDialectResolutionInfo info = TestingDialectResolutionInfo.forDatabaseInfo( productName, + null, + NO_VERSION, + NO_VERSION, + configurationValues + ); + + assertThat( info.getDatabaseMetadata() ).isNull(); + + return new CommunityDialectResolver().resolveDialect( info ); + } +}