Skip to content

HHH-19297 Register SingleStore json functions to community SingleStoreDialect #9927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@
import org.hibernate.boot.model.relational.Exportable;
import org.hibernate.boot.model.relational.Sequence;
import org.hibernate.boot.model.relational.SqlStringGenerationContext;
import org.hibernate.community.dialect.function.json.SingleStoreJsonArrayAggFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonArrayAppendFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonArrayFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonArrayInsertFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonExistsFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonMergepatchFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonObjectAggFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonObjectFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonQueryFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonRemoveFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonSetFunction;
import org.hibernate.community.dialect.function.json.SingleStoreJsonValueFunction;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.DmlTargetColumnQualifierSupport;
Expand Down Expand Up @@ -65,14 +77,14 @@
import org.hibernate.mapping.UniqueKey;
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
import org.hibernate.query.common.TemporalUnit;
import org.hibernate.query.sqm.CastType;
import org.hibernate.query.sqm.IntervalType;
import org.hibernate.query.common.TemporalUnit;
import org.hibernate.query.sqm.function.SqmFunctionRegistry;
import org.hibernate.query.sqm.mutation.spi.AfterUseAction;
import org.hibernate.query.sqm.mutation.spi.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.AfterUseAction;
import org.hibernate.query.sqm.mutation.spi.BeforeUseAction;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
import org.hibernate.query.sqm.produce.function.FunctionParameterType;
Expand All @@ -99,6 +111,7 @@
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 org.hibernate.type.spi.TypeConfiguration;

import jakarta.persistence.TemporalType;

Expand Down Expand Up @@ -323,7 +336,7 @@
break;
case TIMESTAMP:
if ( temporalAccessor instanceof ZonedDateTime ) {
temporalAccessor = ( (ZonedDateTime) temporalAccessor ).toOffsetDateTime();
temporalAccessor = ((ZonedDateTime) temporalAccessor).toOffsetDateTime();
}
appender.appendSql( "timestamp('" );
appendAsTimestampWithMicros(
Expand Down Expand Up @@ -415,15 +428,16 @@
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;
} );
private static final ViolatedConstraintNameExtractor EXTRACTOR = new TemplatedViolatedConstraintNameExtractor(
sqle -> {
final String sqlState = JdbcExceptionHelper.extractSqlState( sqle );
if ( sqlState != null ) {
if ( Integer.parseInt( sqlState ) == 23000 ) {

Check notice

Code scanning / CodeQL

Missing catch of NumberFormatException Note

Potential uncaught 'java.lang.NumberFormatException'.
return extractUsingTemplate( " for key '", "'", sqle.getMessage() );
}
}
return null;
} );

@Override
public boolean qualifyIndexName() {
Expand Down Expand Up @@ -518,14 +532,18 @@
.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() );
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 ) );
Expand Down Expand Up @@ -582,23 +600,18 @@
commonFunctionFactory.hypotheticalOrderedSetAggregates_windowEmulation();
commonFunctionFactory.inverseDistributionOrderedSetAggregates_windowEmulation();
commonFunctionFactory.listagg_groupConcat();
functionContributions.getFunctionRegistry()
.namedDescriptorBuilder( "time" )
SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry();
final TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration();
BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry();
functionRegistry.namedDescriptorBuilder( "time" )
.setExactArgumentCount( 1 )
.setInvariantType( functionContributions.getTypeConfiguration()
.getBasicTypeRegistry()
.resolve( StandardBasicTypes.STRING ) )
.setInvariantType( basicTypeRegistry.resolve( StandardBasicTypes.STRING ) )
.register();
functionContributions.getFunctionRegistry()
.patternDescriptorBuilder( "median", "median(?1) over ()" )
.setInvariantType( functionContributions.getTypeConfiguration()
.getBasicTypeRegistry()
.resolve( StandardBasicTypes.DOUBLE ) )
functionRegistry.patternDescriptorBuilder( "median", "median(?1) over ()" )
.setInvariantType( basicTypeRegistry.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 )
Expand All @@ -611,6 +624,18 @@
.setParameterTypes( FunctionParameterType.INTEGER )
.register();
functionRegistry.registerAlternateKey( "char", "chr" );
functionRegistry.register( "json_object", new SingleStoreJsonObjectFunction( typeConfiguration ) );
functionRegistry.register( "json_array", new SingleStoreJsonArrayFunction( typeConfiguration ) );
functionRegistry.register( "json_value", new SingleStoreJsonValueFunction( typeConfiguration ) );
functionRegistry.register( "json_exists", new SingleStoreJsonExistsFunction( typeConfiguration ) );
functionRegistry.register( "json_query", new SingleStoreJsonQueryFunction( typeConfiguration ) );
functionRegistry.register( "json_arrayagg", new SingleStoreJsonArrayAggFunction( typeConfiguration ) );
functionRegistry.register( "json_objectagg", new SingleStoreJsonObjectAggFunction( typeConfiguration ) );
functionRegistry.register( "json_set", new SingleStoreJsonSetFunction( typeConfiguration ) );
functionRegistry.register( "json_remove", new SingleStoreJsonRemoveFunction( typeConfiguration ) );
functionRegistry.register( "json_mergepatch", new SingleStoreJsonMergepatchFunction( typeConfiguration ) );
functionRegistry.register( "json_array_append", new SingleStoreJsonArrayAppendFunction( typeConfiguration ) );
functionRegistry.register( "json_array_insert", new SingleStoreJsonArrayInsertFunction( typeConfiguration ) );
}


Expand Down Expand Up @@ -940,7 +965,8 @@

@Override
public String getDropForeignKeyString() {
throw new UnsupportedOperationException( "SingleStore does not support foreign keys and referential integrity" );
throw new UnsupportedOperationException(
"SingleStore does not support foreign keys and referential integrity" );
}

@Override
Expand Down Expand Up @@ -986,12 +1012,12 @@

@Override
public String[] getCreateCatalogCommand(String catalogName) {
return new String[] { "create database " + catalogName };
return new String[] {"create database " + catalogName};
}

@Override
public String[] getDropCatalogCommand(String catalogName) {
return new String[] { "drop database " + catalogName };
return new String[] {"drop database " + catalogName};
}

@Override
Expand Down Expand Up @@ -1211,13 +1237,14 @@
String referencedTable,
String[] primaryKey,
boolean referencesPrimaryKey) {
throw new UnsupportedOperationException( "SingleStore does not support foreign keys and referential integrity." );
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." );
public String getAddForeignKeyConstraintString(String constraintName, String foreignKeyDefinition) {
throw new UnsupportedOperationException(
"SingleStore does not support foreign keys and referential integrity." );
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.DmlTargetColumnQualifierSupport;
import org.hibernate.dialect.sql.ast.MySQLSqlAstTranslator;
import org.hibernate.engine.jdbc.Size;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.Stack;
import org.hibernate.query.sqm.ComparisonOperator;
Expand Down Expand Up @@ -49,6 +51,7 @@
*/
public class SingleStoreSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {

private static final int MAX_CHAR_SIZE = 8192;
private final SingleStoreDialect dialect;

public SingleStoreSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement, SingleStoreDialect dialect) {
Expand Down Expand Up @@ -106,7 +109,8 @@ protected void visitInsertSource(InsertSelectStatement statement) {
@Override
public void visitColumnReference(ColumnReference columnReference) {
final Statement currentStatement;
if ( "excluded".equals( columnReference.getQualifier() ) && ( currentStatement = getStatementStack().getCurrent() ) instanceof InsertSelectStatement && ( (InsertSelectStatement) currentStatement ).getSourceSelectStatement() == null ) {
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 );
Expand Down Expand Up @@ -169,8 +173,8 @@ protected String determineColumnReferenceQualifier(ColumnReference columnReferen
// 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() ) ) {
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
Expand Down Expand Up @@ -202,7 +206,8 @@ public void visitBooleanExpressionPredicate(BooleanExpressionPredicate booleanEx

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(
return useOffsetFetchClause(
queryPart ) && getQueryPartForRowNumbering() != queryPart && supportsWindowFunctions() && !isRowsOnlyFetchClauseType(
queryPart );
}

Expand Down Expand Up @@ -323,8 +328,9 @@ protected void emulateTupleComparison(
@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" );
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 );
Expand Down Expand Up @@ -376,7 +382,7 @@ protected void renderBackslashEscapedLikePattern(
// 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();
Object literalValue = ((Literal) pattern).getLiteralValue();
if ( literalValue == null ) {
pattern.accept( this );
}
Expand All @@ -403,9 +409,60 @@ private boolean supportsWindowFunctions() {
return true;
}

public static String getSqlType(CastTarget castTarget, SessionFactoryImplementor factory) {
final String sqlType = getCastTypeName( castTarget, factory.getTypeConfiguration() );
return getSqlType( castTarget, sqlType, factory.getJdbcServices().getDialect() );
}

private static String getSqlType(CastTarget castTarget, String sqlType, Dialect dialect) {
if ( sqlType != null ) {
int parenthesesIndex = sqlType.indexOf( '(' );
final String baseName = parenthesesIndex == -1 ? sqlType : sqlType.substring( 0, parenthesesIndex ).trim();
switch ( baseName.toLowerCase( Locale.ROOT ) ) {
case "bit":
return "unsigned";
case "tinyint":
case "smallint":
case "integer":
case "bigint":
return "signed";
case "float":
case "real":
case "double precision":
final int precision = castTarget.getPrecision() == null ?
dialect.getDefaultDecimalPrecision() :
castTarget.getPrecision();
final int scale = castTarget.getScale() == null ? Size.DEFAULT_SCALE : castTarget.getScale();
return "decimal(" + precision + "," + scale + ")";
case "char":
case "varchar":
case "text":
case "mediumtext":
case "longtext":
case "set":
case "enum":
if ( castTarget.getLength() == null ) {
if ( castTarget.getJdbcMapping().getJdbcJavaType().getJavaType() == Character.class ) {
return "char(1)";
}
else {
return "char";
}
}
return castTarget.getLength() > MAX_CHAR_SIZE ? "char" : "char(" + castTarget.getLength() + ")";
case "binary":
case "varbinary":
case "mediumblob":
case "longblob":
return castTarget.getLength() == null ? "binary" : "binary(" + castTarget.getLength() + ")";
}
}
return sqlType;
}

@Override
public void visitCastTarget(CastTarget castTarget) {
String sqlType = MySQLSqlAstTranslator.getSqlType( castTarget, getSessionFactory() );
String sqlType = getSqlType( castTarget, getSessionFactory() );
if ( sqlType != null ) {
appendSql( sqlType );
}
Expand Down
Loading