HHH-18459 Add SingleStore dialect

This commit is contained in:
oyeliseiev-ua 2024-08-08 18:56:18 +03:00 committed by Christian Beikov
parent 5c0058e6bd
commit ac4931a264
6 changed files with 1949 additions and 0 deletions

View File

@ -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";
}
};
/**

View File

@ -118,6 +118,8 @@ public class CommunityDialectSelector implements DialectSelector {
return Teradata14Dialect.class;
case "TimesTen":
return TimesTenDialect.class;
case "SingleStore":
return SingleStoreDialect.class;
}
return null;
}

View File

@ -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<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
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<SqlSelection> 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<ColumnReference> targetColumnReferences = statement.getTargetColumns();
final List<String> 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<Clause> 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<? extends SqlAstNode> lhsExpressions,
final List<? extends SqlAstNode> 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 );
}
}
}

View File

@ -72,6 +72,7 @@ public class CommunityDialectSelectorTest {
testDialectNamingResolution( TeradataDialect.class );
testDialectNamingResolution( TimesTenDialect.class );
testDialectNamingResolution( SingleStoreDialect.class );
}
private void testDialectNamingResolution(final Class<?> dialectClass) {

View File

@ -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<Map<String, Object>> configurationProvider) {
final Map<String, Object> 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 );
}
}