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 index 9714b4728dfb..00d04e455de1 100644 --- 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 @@ -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; @@ -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; @@ -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; @@ -323,7 +336,7 @@ public void appendDateTimeLiteral( break; case TIMESTAMP: if ( temporalAccessor instanceof ZonedDateTime ) { - temporalAccessor = ( (ZonedDateTime) temporalAccessor ).toOffsetDateTime(); + temporalAccessor = ((ZonedDateTime) temporalAccessor).toOffsetDateTime(); } appender.appendSql( "timestamp('" ); appendAsTimestampWithMicros( @@ -415,15 +428,16 @@ 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; - } ); + 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() { @@ -518,14 +532,18 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR .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 ) ); @@ -582,23 +600,18 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio 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 ) @@ -611,6 +624,18 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio .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 ) ); } @@ -940,7 +965,8 @@ public static Replacer datetimeFormat(String format) { @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 @@ -986,12 +1012,12 @@ public boolean canCreateCatalog() { @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 @@ -1211,13 +1237,14 @@ public String getAddForeignKeyConstraintString( 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 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 index 14264326aab9..965499eceac8 100644 --- 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 @@ -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; @@ -49,6 +51,7 @@ */ public class SingleStoreSqlAstTranslator extends AbstractSqlAstTranslator { + private static final int MAX_CHAR_SIZE = 8192; private final SingleStoreDialect dialect; public SingleStoreSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement, SingleStoreDialect dialect) { @@ -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 ); @@ -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 @@ -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 ); } @@ -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 ); @@ -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 ); } @@ -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 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAggFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAggFunction.java new file mode 100644 index 000000000000..a9e46b701892 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAggFunction.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.JsonArrayAggFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_arrayagg function. + */ +public class SingleStoreJsonArrayAggFunction extends JsonArrayAggFunction { + + public SingleStoreJsonArrayAggFunction(TypeConfiguration typeConfiguration) { + super( false, typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + List withinGroup, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + sqlAppender.appendSql( "concat('[',group_concat(" ); + final JsonNullBehavior nullBehavior; + if ( sqlAstArguments.size() > 1 ) { + nullBehavior = (JsonNullBehavior) sqlAstArguments.get( 1 ); + } + else { + nullBehavior = JsonNullBehavior.ABSENT; + } + final SqlAstNode firstArg = sqlAstArguments.get( 0 ); + final Expression arg; + if ( firstArg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + arg = ( (Distinct) firstArg ).getExpression(); + } + else { + arg = (Expression) firstArg; + } + if ( caseWrapper ) { + if ( nullBehavior != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_arrayagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arg, nullBehavior, translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arg, nullBehavior, translator ); + } + if ( withinGroup != null && !withinGroup.isEmpty() ) { + translator.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( translator ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( translator ); + } + translator.getCurrentClauseStack().pop(); + } + sqlAppender.appendSql( " separator ','),']')" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, Expression arg, JsonNullBehavior nullBehavior, SqlAstTranslator translator) { + sqlAppender.appendSql( "to_json(" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + arg.accept( translator ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAppendFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAppendFunction.java new file mode 100644 index 000000000000..7b455882d39f --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayAppendFunction.java @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.AbstractJsonArrayAppendFunction; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_array_append function. + */ +public class SingleStoreJsonArrayAppendFunction extends AbstractJsonArrayAppendFunction { + + public SingleStoreJsonArrayAppendFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( + jsonPath ) ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_set_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( ',' ); + sqlAppender.appendSql( " case when json_get_type(json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( ")) = 'array' THEN " ); + buildJsonArrayPushValue( sqlAppender, value ); + sqlAppender.appendSql( "json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( ")," ); + value.accept( translator ); + sqlAppender.appendSql( ") ELSE " ); + buildJsonArrayPushValue( sqlAppender, value ); + sqlAppender.appendSql( "json_build_array(json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( "))," ); + value.accept( translator ); + sqlAppender.appendSql( ") END )" ); + } + + private static boolean isNumeric(SqlAstNode value) { + return value instanceof UnparsedNumericLiteral; + } + + private static void buildJsonArrayPushValue(SqlAppender sqlAppender, SqlAstNode value) { + sqlAppender.appendSql( "json_array_push_" ); + sqlAppender.appendSql( isNumeric( value ) ? "double(" : "string(" ); + } + + private static void buildJsonPath( + SqlAppender sqlAppender, Expression jsonPath, List jsonPathElements) { + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayFunction.java new file mode 100644 index 000000000000..884ce60c241d --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayFunction.java @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.json.JsonArrayFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_array function. + */ +public class SingleStoreJsonArrayFunction extends JsonArrayFunction { + + public SingleStoreJsonArrayFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "json_build_array()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = null; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select json_agg(t.v order by t.i) from (select 0 i, to_json(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ") v" ); + for ( int i = 1; i < argumentsCount; i++ ) { + sqlAppender.appendSql( " union all select " ); + sqlAppender.appendSql( i ); + sqlAppender.appendSql( ", to_json(" ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( ')' ); + } + sqlAppender.appendSql( ") t where t.v is not null)" ); + } + else { + sqlAppender.appendSql( "json_build_array" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayInsertFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayInsertFunction.java new file mode 100644 index 000000000000..22cb2932947c --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonArrayInsertFunction.java @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.AbstractJsonArrayInsertFunction; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_array_insert function. + */ +public class SingleStoreJsonArrayInsertFunction extends AbstractJsonArrayInsertFunction { + + public SingleStoreJsonArrayInsertFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( + jsonPath ) ); + final SqlAstNode value = arguments.get( 2 ); + final int arrayIndex = getArrayIndex( jsonPathElements ); + if ( jsonPathElements.size() > 1 ) { + sqlAppender.appendSql( "json_set_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( ", " ); + } + sqlAppender.appendSql( "case when json_get_type(json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( ")) = 'array' THEN " ); + buildJsonArrayInsertValue( sqlAppender, value ); + sqlAppender.appendSql( "json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( "), " ); + sqlAppender.appendSql( arrayIndex ); + sqlAppender.appendSql( ", 0, " ); + value.accept( translator ); + sqlAppender.appendSql( ") ELSE " ); + buildJsonArrayInsertValue( sqlAppender, value ); + sqlAppender.appendSql( "json_build_array(json_extract_json(" ); + json.accept( translator ); + buildJsonPath( sqlAppender, jsonPath, jsonPathElements ); + sqlAppender.appendSql( "))," ); + sqlAppender.appendSql( arrayIndex ); + sqlAppender.appendSql( ", 0, " ); + value.accept( translator ); + sqlAppender.appendSql( ") END" ); + if ( jsonPathElements.size() > 1 ) { + sqlAppender.appendSql( ')' ); + } + } + + private static int getArrayIndex(List jsonPathElements) { + if ( jsonPathElements.isEmpty() ) { + throw new QueryException( "SingleStore json_array_insert function requires at least one json path element" ); + } + JsonPathHelper.JsonPathElement lastPathElement = jsonPathElements.get( jsonPathElements.size() - 1 ); + if ( !( lastPathElement instanceof JsonPathHelper.JsonIndexAccess ) ) { + throw new QueryException( + "SingleStore json_array_insert function last path parameter must be an array index element" ); + } + return ( (JsonPathHelper.JsonIndexAccess) lastPathElement ).index(); + } + + private static boolean isNumeric(SqlAstNode value) { + return value instanceof UnparsedNumericLiteral; + } + + private static void buildJsonArrayInsertValue(SqlAppender sqlAppender, SqlAstNode value) { + sqlAppender.appendSql( "json_splice_" ); + sqlAppender.appendSql( isNumeric( value ) ? "double(" : "string(" ); + } + + private static void buildJsonPath( + SqlAppender sqlAppender, Expression jsonPath, List jsonPathElements) { + for ( int i = 0; i < jsonPathElements.size() - 1; i++ ) { + JsonPathHelper.JsonPathElement pathElement = jsonPathElements.get( i ); + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonExistsFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonExistsFunction.java new file mode 100644 index 000000000000..3051a52c477e --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonExistsFunction.java @@ -0,0 +1,63 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.JsonExistsFunction; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonExistsErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_exists function. + */ +public class SingleStoreJsonExistsFunction extends JsonExistsFunction { + + public SingleStoreJsonExistsFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonExistsArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonExistsErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on SingleStore" ); + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "SingleStore json_exists only support literal json paths, but got " + arguments.jsonPath() ); + } + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + sqlAppender.appendSql( "json_match_any_exists(" ); + arguments.jsonDocument().accept( walker ); + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonMergepatchFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonMergepatchFunction.java new file mode 100644 index 000000000000..6137552b97cc --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonMergepatchFunction.java @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.json.AbstractJsonMergepatchFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_mergepatch function. + */ +public class SingleStoreJsonMergepatchFunction extends AbstractJsonMergepatchFunction { + + public SingleStoreJsonMergepatchFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final int argumentCount = arguments.size(); + for ( int i = 0; i < argumentCount - 1; i++ ) { + sqlAppender.appendSql( "json_merge_patch(" ); + } + arguments.get( 0 ).accept( translator ); + for ( int i = 1; i < argumentCount; i++ ) { + sqlAppender.appendSql( ',' ); + arguments.get( i ).accept( translator ); + sqlAppender.appendSql( ')' ); + } + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectAggFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectAggFunction.java new file mode 100644 index 000000000000..4800db04d79c --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectAggFunction.java @@ -0,0 +1,73 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.JsonObjectAggFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.sql.ast.tree.expression.JsonObjectAggUniqueKeysBehavior; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_objectagg function. + */ +public class SingleStoreJsonObjectAggFunction extends JsonObjectAggFunction { + + public SingleStoreJsonObjectAggFunction(TypeConfiguration typeConfiguration) { + super( ",", false, typeConfiguration ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonObjectAggArguments arguments, + Predicate filter, + ReturnableType returnType, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null; + if ( arguments.uniqueKeysBehavior() == JsonObjectAggUniqueKeysBehavior.WITH ) { + throw new QueryException( "Can't emulate json_objectagg 'with unique keys' clause." ); + } + sqlAppender.appendSql( "concat('{',group_concat(concat(to_json(" ); + arguments.key().accept( translator ); + sqlAppender.appendSql( "),':'," ); + if ( caseWrapper ) { + if ( arguments.nullBehavior() != JsonNullBehavior.ABSENT ) { + throw new QueryException( "Can't emulate json_objectagg filter clause when using 'null on null' clause." ); + } + translator.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + translator.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + sqlAppender.appendSql( " else null end)" ); + } + else { + renderArgument( sqlAppender, arguments.value(), arguments.nullBehavior(), translator ); + } + sqlAppender.appendSql( ") separator ','),'}')" ); + } + + @Override + protected void renderArgument( + SqlAppender sqlAppender, Expression arg, JsonNullBehavior nullBehavior, SqlAstTranslator translator) { + sqlAppender.appendSql( "to_json(" ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( "nullif(" ); + } + arg.accept( translator ); + if ( nullBehavior != JsonNullBehavior.NULL ) { + sqlAppender.appendSql( ",'null')" ); + } + sqlAppender.appendSql( ")" ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectFunction.java new file mode 100644 index 000000000000..f08bc5d8a0fd --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonObjectFunction.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.dialect.function.json.JsonObjectFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.JsonNullBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_object function. + */ +public class SingleStoreJsonObjectFunction extends JsonObjectFunction { + + public SingleStoreJsonObjectFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, false ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( sqlAstArguments.isEmpty() ) { + sqlAppender.appendSql( "json_build_object()" ); + } + else { + final SqlAstNode lastArgument = sqlAstArguments.get( sqlAstArguments.size() - 1 ); + final JsonNullBehavior nullBehavior; + final int argumentsCount; + if ( lastArgument instanceof JsonNullBehavior ) { + nullBehavior = (JsonNullBehavior) lastArgument; + argumentsCount = sqlAstArguments.size() - 1; + } + else { + nullBehavior = JsonNullBehavior.NULL; + argumentsCount = sqlAstArguments.size(); + } + if ( nullBehavior == JsonNullBehavior.ABSENT ) { + sqlAppender.appendSql( "(select concat('{', group_concat(concat(t.k, ':', t.v)), '}') from (select " ); + sqlAppender.appendSql( "to_json(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.appendSql( ") k, " ); + sqlAppender.appendSql( "to_json(" ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ") v" ); + for ( int i = 2; i < argumentsCount; i += 2 ) { + sqlAppender.appendSql( " union all select " ); + sqlAppender.appendSql( "to_json(" ); + sqlAstArguments.get( i ).accept( walker ); + sqlAppender.appendSql( ")," ); + sqlAppender.appendSql( "to_json(" ); + sqlAstArguments.get( i + 1 ).accept( walker ); + sqlAppender.appendSql( ")" ); + } + sqlAppender.appendSql( ") t where t.v <> to_json(null))" ); + } + else { + sqlAppender.appendSql( "json_build_object" ); + char separator = '('; + for ( int i = 0; i < argumentsCount; i++ ) { + sqlAppender.appendSql( separator ); + sqlAstArguments.get( i ).accept( walker ); + separator = ','; + } + sqlAppender.appendSql( ')' ); + } + } + } + +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonQueryFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonQueryFunction.java new file mode 100644 index 000000000000..7d78dcf7d24c --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonQueryFunction.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.dialect.function.json.JsonQueryFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonQueryEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryErrorBehavior; +import org.hibernate.sql.ast.tree.expression.JsonQueryWrapMode; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_query function. + */ +public class SingleStoreJsonQueryFunction extends JsonQueryFunction { + + public SingleStoreJsonQueryFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonQueryArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonQueryErrorBehavior.ERROR ) { + throw new QueryException( "Can't emulate on error clause on SingleStore" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonQueryEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on SingleStore" ); + } + else { + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "SingleStore json_query only support literal json paths, but got " + arguments.jsonPath() ); + } + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + final JsonQueryWrapMode wrapMode = arguments.wrapMode(); + final DecorationMode decorationMode = determineDecorationMode( wrapMode ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( "concat('['," ); + } + sqlAppender.appendSql( "nullif(json_extract_string(" ); + arguments.jsonDocument().accept( walker ); + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + sqlAppender.appendSql( "),'null')" ); + if ( decorationMode == DecorationMode.WRAP ) { + sqlAppender.appendSql( ",']')" ); + } + } + } + + enum DecorationMode {NONE, WRAP} + + private static DecorationMode determineDecorationMode(JsonQueryWrapMode wrapMode) { + if ( wrapMode == JsonQueryWrapMode.WITH_WRAPPER ) { + return DecorationMode.WRAP; + } + return DecorationMode.NONE; + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonRemoveFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonRemoveFunction.java new file mode 100644 index 000000000000..aad9d386badc --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonRemoveFunction.java @@ -0,0 +1,57 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.AbstractJsonRemoveFunction; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_remove function. + */ +public class SingleStoreJsonRemoveFunction extends AbstractJsonRemoveFunction { + + public SingleStoreJsonRemoveFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( + jsonPath ) ); + sqlAppender.appendSql( "json_delete_key(" ); + json.accept( translator ); + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + sqlAppender.appendSql( ')' ); + } +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonSetFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonSetFunction.java new file mode 100644 index 000000000000..1ef892573469 --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonSetFunction.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.AbstractJsonSetFunction; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.UnparsedNumericLiteral; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_set function. + *

+ * The behavior of creating missing keypaths is available by setting the json_compatibility_level to 8.0 + */ +public class SingleStoreJsonSetFunction extends AbstractJsonSetFunction { + + public SingleStoreJsonSetFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List arguments, + ReturnableType returnType, + SqlAstTranslator translator) { + final Expression json = (Expression) arguments.get( 0 ); + final Expression jsonPath = (Expression) arguments.get( 1 ); + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( translator.getLiteralValue( + jsonPath ) ); + final SqlAstNode value = arguments.get( 2 ); + sqlAppender.appendSql( "json_set_" ); + sqlAppender.appendSql( isNumeric( value ) ? "double(" : "string(" ); + json.accept( translator ); + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + sqlAppender.appendSql( ',' ); + value.accept( translator ); + sqlAppender.appendSql( ')' ); + } + + private static boolean isNumeric(SqlAstNode value) { + return value instanceof UnparsedNumericLiteral; + } + +} diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonValueFunction.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonValueFunction.java new file mode 100644 index 000000000000..5f713a4fd87d --- /dev/null +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/function/json/SingleStoreJsonValueFunction.java @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.community.dialect.function.json; + +import java.util.List; + +import org.hibernate.QueryException; +import org.hibernate.dialect.function.json.JsonPathHelper; +import org.hibernate.dialect.function.json.JsonValueFunction; +import org.hibernate.metamodel.model.domain.ReturnableType; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.expression.JsonValueEmptyBehavior; +import org.hibernate.sql.ast.tree.expression.JsonValueErrorBehavior; +import org.hibernate.type.spi.TypeConfiguration; + +/** + * SingleStore json_value function. + */ +public class SingleStoreJsonValueFunction extends JsonValueFunction { + + public SingleStoreJsonValueFunction(TypeConfiguration typeConfiguration) { + super( typeConfiguration, true, false ); + } + + @Override + protected void render( + SqlAppender sqlAppender, + JsonValueArguments arguments, + ReturnableType returnType, + SqlAstTranslator walker) { + + if ( arguments.errorBehavior() != null && arguments.errorBehavior() != JsonValueErrorBehavior.NULL ) { + throw new QueryException( "Can't emulate on error clause on SingleStore" ); + } + if ( arguments.emptyBehavior() != null && arguments.emptyBehavior() != JsonValueEmptyBehavior.NULL ) { + throw new QueryException( "Can't emulate on empty clause on SingleStore" ); + } + if ( arguments.returningType() != null ) { + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( "case " ); + } + else { + sqlAppender.append( "cast(" ); + } + } + final String jsonPath; + try { + jsonPath = walker.getLiteralValue( arguments.jsonPath() ); + } + catch (Exception ex) { + throw new QueryException( "SingleStore json_value only support literal json paths, but got " + arguments.jsonPath() ); + } + final List jsonPathElements = JsonPathHelper.parseJsonPathElements( jsonPath ); + sqlAppender.appendSql( "json_extract_string(" ); + arguments.jsonDocument().accept( walker ); + for ( JsonPathHelper.JsonPathElement pathElement : jsonPathElements ) { + sqlAppender.appendSql( ',' ); + if ( pathElement instanceof JsonPathHelper.JsonAttribute attribute ) { + sqlAppender.appendSingleQuoteEscapedString( attribute.attribute() ); + } + else if ( pathElement instanceof JsonPathHelper.JsonParameterIndexAccess ) { + final String parameterName = ( (JsonPathHelper.JsonParameterIndexAccess) pathElement ).parameterName(); + throw new QueryException( "JSON path [" + jsonPath + "] uses parameter [" + parameterName + "] that is not passed" ); + } + else { + sqlAppender.appendSql( '\'' ); + sqlAppender.appendSql( ( (JsonPathHelper.JsonIndexAccess) pathElement ).index() ); + sqlAppender.appendSql( '\'' ); + } + } + sqlAppender.appendSql( ')' ); + if ( arguments.returningType() != null ) { + if ( arguments.returningType().getJdbcMapping().getJdbcType().isBoolean() ) { + sqlAppender.append( " when 'true' then true when 'false' then false end " ); + } + else { + sqlAppender.appendSql( " as " ); + arguments.returningType().accept( walker ); + sqlAppender.appendSql( ')' ); + } + } + } +}