Skip to content

Commit 9eba927

Browse files
committed
HHH-18794 Add JSON aggregate support for MariaDB
1 parent b73a4b0 commit 9eba927

File tree

9 files changed

+177
-21
lines changed

9 files changed

+177
-21
lines changed

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
1919
import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder;
2020
import org.hibernate.engine.spi.SessionFactoryImplementor;
21+
import org.hibernate.query.sqm.CastType;
2122
import org.hibernate.service.ServiceRegistry;
2223
import org.hibernate.sql.ast.SqlAstTranslator;
2324
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@@ -29,8 +30,6 @@
2930
import org.hibernate.type.SqlTypes;
3031
import org.hibernate.type.StandardBasicTypes;
3132
import org.hibernate.type.descriptor.jdbc.JdbcType;
32-
import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcTypeConstructor;
33-
import org.hibernate.type.descriptor.jdbc.JsonJdbcType;
3433
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
3534
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
3635
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
@@ -150,15 +149,22 @@ public JdbcType resolveSqlTypeDescriptor(
150149
public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
151150
final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry();
152151
// Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting
153-
jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE );
154-
jdbcTypeRegistry.addTypeConstructorIfAbsent( JsonArrayJdbcTypeConstructor.INSTANCE );
152+
jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MariaDBCastingJsonJdbcType.INSTANCE );
153+
jdbcTypeRegistry.addTypeConstructorIfAbsent( MariaDBCastingJsonArrayJdbcTypeConstructor.INSTANCE );
155154

156155
super.contributeTypes( typeContributions, serviceRegistry );
157156
if ( getVersion().isSameOrAfter( 10, 7 ) ) {
158157
jdbcTypeRegistry.addDescriptorIfAbsent( VarcharUUIDJdbcType.INSTANCE );
159158
}
160159
}
161160

161+
@Override
162+
public String castPattern(CastType from, CastType to) {
163+
return to == CastType.JSON
164+
? "json_extract(?1,'$')"
165+
: super.castPattern( from, to );
166+
}
167+
162168
@Override
163169
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
164170
return new StandardSqlAstTranslatorFactory() {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect;
6+
7+
import org.hibernate.sql.ast.spi.SqlAppender;
8+
import org.hibernate.type.descriptor.jdbc.JdbcType;
9+
import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType;
10+
11+
/**
12+
* @author Christian Beikov
13+
*/
14+
public class MariaDBCastingJsonArrayJdbcType extends JsonArrayJdbcType {
15+
16+
public MariaDBCastingJsonArrayJdbcType(JdbcType elementJdbcType) {
17+
super( elementJdbcType );
18+
}
19+
20+
@Override
21+
public void appendWriteExpression(
22+
String writeExpression,
23+
SqlAppender appender,
24+
Dialect dialect) {
25+
appender.append( "json_extract(" );
26+
appender.append( writeExpression );
27+
appender.append( ",'$')" );
28+
}
29+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect;
6+
7+
import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation;
8+
import org.hibernate.type.BasicType;
9+
import org.hibernate.type.SqlTypes;
10+
import org.hibernate.type.descriptor.jdbc.JdbcType;
11+
import org.hibernate.type.descriptor.jdbc.JdbcTypeConstructor;
12+
import org.hibernate.type.spi.TypeConfiguration;
13+
14+
/**
15+
* Factory for {@link MariaDBCastingJsonArrayJdbcType}.
16+
*/
17+
public class MariaDBCastingJsonArrayJdbcTypeConstructor implements JdbcTypeConstructor {
18+
19+
public static final MariaDBCastingJsonArrayJdbcTypeConstructor INSTANCE = new MariaDBCastingJsonArrayJdbcTypeConstructor();
20+
21+
@Override
22+
public JdbcType resolveType(
23+
TypeConfiguration typeConfiguration,
24+
Dialect dialect,
25+
BasicType<?> elementType,
26+
ColumnTypeInformation columnTypeInformation) {
27+
return resolveType( typeConfiguration, dialect, elementType.getJdbcType(), columnTypeInformation );
28+
}
29+
30+
@Override
31+
public JdbcType resolveType(
32+
TypeConfiguration typeConfiguration,
33+
Dialect dialect,
34+
JdbcType elementType,
35+
ColumnTypeInformation columnTypeInformation) {
36+
return new MariaDBCastingJsonArrayJdbcType( elementType );
37+
}
38+
39+
@Override
40+
public int getDefaultSqlTypeCode() {
41+
return SqlTypes.JSON_ARRAY;
42+
}
43+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect;
6+
7+
import org.hibernate.metamodel.mapping.EmbeddableMappingType;
8+
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
9+
import org.hibernate.sql.ast.spi.SqlAppender;
10+
import org.hibernate.type.descriptor.jdbc.AggregateJdbcType;
11+
import org.hibernate.type.descriptor.jdbc.JsonJdbcType;
12+
13+
/**
14+
* @author Christian Beikov
15+
*/
16+
public class MariaDBCastingJsonJdbcType extends JsonJdbcType {
17+
/**
18+
* Singleton access
19+
*/
20+
public static final JsonJdbcType INSTANCE = new MariaDBCastingJsonJdbcType( null );
21+
22+
public MariaDBCastingJsonJdbcType(EmbeddableMappingType embeddableMappingType) {
23+
super( embeddableMappingType );
24+
}
25+
26+
@Override
27+
public AggregateJdbcType resolveAggregateJdbcType(
28+
EmbeddableMappingType mappingType,
29+
String sqlType,
30+
RuntimeModelCreationContext creationContext) {
31+
return new MariaDBCastingJsonJdbcType( mappingType );
32+
}
33+
34+
@Override
35+
public void appendWriteExpression(
36+
String writeExpression,
37+
SqlAppender appender,
38+
Dialect dialect) {
39+
appender.append( "json_extract(" );
40+
appender.append( writeExpression );
41+
appender.append( ",'$')" );
42+
}
43+
}

hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import org.hibernate.boot.model.FunctionContributions;
1111
import org.hibernate.boot.model.TypeContributions;
12+
import org.hibernate.dialect.aggregate.AggregateSupport;
13+
import org.hibernate.dialect.aggregate.MySQLAggregateSupport;
1214
import org.hibernate.dialect.function.CommonFunctionFactory;
1315
import org.hibernate.dialect.identity.IdentityColumnSupport;
1416
import org.hibernate.dialect.identity.MariaDBIdentityColumnSupport;
@@ -19,6 +21,7 @@
1921
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
2022
import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder;
2123
import org.hibernate.engine.spi.SessionFactoryImplementor;
24+
import org.hibernate.query.sqm.CastType;
2225
import org.hibernate.service.ServiceRegistry;
2326
import org.hibernate.sql.ast.SqlAstTranslator;
2427
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@@ -30,8 +33,6 @@
3033
import org.hibernate.type.SqlTypes;
3134
import org.hibernate.type.StandardBasicTypes;
3235
import org.hibernate.type.descriptor.jdbc.JdbcType;
33-
import org.hibernate.type.descriptor.jdbc.JsonArrayJdbcTypeConstructor;
34-
import org.hibernate.type.descriptor.jdbc.JsonJdbcType;
3536
import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry;
3637
import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl;
3738
import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry;
@@ -122,6 +123,11 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR
122123
}
123124
}
124125

126+
@Override
127+
public AggregateSupport getAggregateSupport() {
128+
return MySQLAggregateSupport.LONGTEXT_INSTANCE;
129+
}
130+
125131
@Override
126132
protected void registerKeyword(String word) {
127133
// The MariaDB driver reports that "STRING" is a keyword, but
@@ -156,16 +162,23 @@ public JdbcType resolveSqlTypeDescriptor(
156162
@Override
157163
public void contributeTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
158164
final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry();
159-
// Make sure we register the JSON type descriptor before calling super, because MariaDB does not need casting
160-
jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, JsonJdbcType.INSTANCE );
161-
jdbcTypeRegistry.addTypeConstructorIfAbsent( JsonArrayJdbcTypeConstructor.INSTANCE );
165+
// Make sure we register the JSON type descriptor before calling super, because MariaDB needs special casting
166+
jdbcTypeRegistry.addDescriptorIfAbsent( SqlTypes.JSON, MariaDBCastingJsonJdbcType.INSTANCE );
167+
jdbcTypeRegistry.addTypeConstructorIfAbsent( MariaDBCastingJsonArrayJdbcTypeConstructor.INSTANCE );
162168

163169
super.contributeTypes( typeContributions, serviceRegistry );
164170
if ( getVersion().isSameOrAfter( 10, 7 ) ) {
165171
jdbcTypeRegistry.addDescriptorIfAbsent( VarcharUUIDJdbcType.INSTANCE );
166172
}
167173
}
168174

175+
@Override
176+
public String castPattern(CastType from, CastType to) {
177+
return to == CastType.JSON
178+
? "json_extract(?1,'$')"
179+
: super.castPattern( from, to );
180+
}
181+
169182
@Override
170183
public SqlAstTranslatorFactory getSqlAstTranslatorFactory() {
171184
return new StandardSqlAstTranslatorFactory() {

hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR
440440

441441
@Override
442442
public AggregateSupport getAggregateSupport() {
443-
return MySQLAggregateSupport.valueOf( this );
443+
return MySQLAggregateSupport.JSON_INSTANCE;
444444
}
445445

446446
@Deprecated(since="6.4")

hibernate-core/src/main/java/org/hibernate/dialect/aggregate/MySQLAggregateSupport.java

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55
package org.hibernate.dialect.aggregate;
66

7-
import org.hibernate.dialect.Dialect;
87
import org.hibernate.internal.util.StringHelper;
98
import org.hibernate.mapping.Column;
109
import org.hibernate.metamodel.mapping.JdbcMapping;
@@ -26,7 +25,9 @@
2625
import static org.hibernate.type.SqlTypes.BOOLEAN;
2726
import static org.hibernate.type.SqlTypes.CHAR;
2827
import static org.hibernate.type.SqlTypes.CLOB;
28+
import static org.hibernate.type.SqlTypes.DOUBLE;
2929
import static org.hibernate.type.SqlTypes.ENUM;
30+
import static org.hibernate.type.SqlTypes.FLOAT;
3031
import static org.hibernate.type.SqlTypes.INTEGER;
3132
import static org.hibernate.type.SqlTypes.JSON;
3233
import static org.hibernate.type.SqlTypes.JSON_ARRAY;
@@ -36,6 +37,7 @@
3637
import static org.hibernate.type.SqlTypes.NCHAR;
3738
import static org.hibernate.type.SqlTypes.NCLOB;
3839
import static org.hibernate.type.SqlTypes.NVARCHAR;
40+
import static org.hibernate.type.SqlTypes.REAL;
3941
import static org.hibernate.type.SqlTypes.SMALLINT;
4042
import static org.hibernate.type.SqlTypes.TIMESTAMP;
4143
import static org.hibernate.type.SqlTypes.TIMESTAMP_UTC;
@@ -45,10 +47,13 @@
4547

4648
public class MySQLAggregateSupport extends AggregateSupportImpl {
4749

48-
private static final AggregateSupport INSTANCE = new MySQLAggregateSupport();
50+
public static final AggregateSupport JSON_INSTANCE = new MySQLAggregateSupport( true );
51+
public static final AggregateSupport LONGTEXT_INSTANCE = new MySQLAggregateSupport( false );
4952

50-
public static AggregateSupport valueOf(Dialect dialect) {
51-
return MySQLAggregateSupport.INSTANCE;
53+
private final boolean jsonType;
54+
55+
public MySQLAggregateSupport(boolean jsonType) {
56+
this.jsonType = jsonType;
5257
}
5358

5459
@Override
@@ -72,7 +77,9 @@ public String aggregateComponentCustomReadExpression(
7277
case BOOLEAN:
7378
return template.replace(
7479
placeholder,
75-
"case " + queryExpression( aggregateParentReadExpression, columnExpression ) + " when 'true' then true when 'false' then false end"
80+
jsonType
81+
? "case " + queryExpression( aggregateParentReadExpression, columnExpression ) + " when cast('true' as json) then true when cast('false' as json) then false end"
82+
: "case " + queryExpression( aggregateParentReadExpression, columnExpression ) + " when 'true' then true when 'false' then false end"
7683
);
7784
case BINARY:
7885
case VARBINARY:
@@ -92,12 +99,18 @@ public String aggregateComponentCustomReadExpression(
9299
throw new IllegalArgumentException( "Unsupported aggregate SQL type: " + aggregateColumnTypeCode );
93100
}
94101

95-
private static String columnCastType(SqlTypedMapping column) {
102+
private String columnCastType(SqlTypedMapping column) {
96103
return switch (column.getJdbcMapping().getJdbcType().getDdlTypeCode()) {
97104
// special case for casting to Boolean
98105
case BOOLEAN, BIT -> "unsigned";
99106
// MySQL doesn't let you cast to INTEGER/BIGINT/TINYINT
100107
case TINYINT, SMALLINT, INTEGER, BIGINT -> "signed";
108+
case REAL -> "float";
109+
case DOUBLE -> "double";
110+
case FLOAT -> jsonType
111+
// In newer versions of MySQL, casting to float/double is supported
112+
? column.getColumnDefinition()
113+
: column.getPrecision() == null || column.getPrecision() == 53 ? "double" : "float";
101114
// MySQL doesn't let you cast to TEXT/LONGTEXT
102115
case CHAR, VARCHAR, LONG32VARCHAR, CLOB, ENUM -> "char";
103116
case NCHAR, NVARCHAR, LONG32NVARCHAR, NCLOB -> "char character set utf8mb4";
@@ -107,12 +120,17 @@ private static String columnCastType(SqlTypedMapping column) {
107120
};
108121
}
109122

110-
private static String valueExpression(String aggregateParentReadExpression, String columnExpression, String columnType) {
123+
private String valueExpression(String aggregateParentReadExpression, String columnExpression, String columnType) {
111124
return "cast(json_unquote(" + queryExpression( aggregateParentReadExpression, columnExpression ) + ") as " + columnType + ')';
112125
}
113126

114-
private static String queryExpression(String aggregateParentReadExpression, String columnExpression) {
115-
return "nullif(json_extract(" + aggregateParentReadExpression + ",'$." + columnExpression + "'),cast('null' as json))";
127+
private String queryExpression(String aggregateParentReadExpression, String columnExpression) {
128+
if ( jsonType ) {
129+
return "nullif(json_extract(" + aggregateParentReadExpression + ",'$." + columnExpression + "'),cast('null' as json))";
130+
}
131+
else {
132+
return "nullif(json_extract(" + aggregateParentReadExpression + ",'$." + columnExpression + "'),'null')";
133+
}
116134
}
117135

118136
private static String jsonCustomWriteExpression(String customWriteExpression, JdbcMapping jdbcMapping) {
@@ -190,7 +208,7 @@ void append(
190208
SqlAstTranslator<?> translator,
191209
AggregateColumnWriteExpression expression);
192210
}
193-
private static class AggregateJsonWriteExpression implements JsonWriteExpression {
211+
private class AggregateJsonWriteExpression implements JsonWriteExpression {
194212
private final LinkedHashMap<String, JsonWriteExpression> subExpressions = new LinkedHashMap<>();
195213

196214
protected void initializeSubExpressions(SelectableMapping[] columns) {
@@ -242,7 +260,7 @@ public void append(
242260
}
243261
}
244262

245-
private static class RootJsonWriteExpression extends AggregateJsonWriteExpression
263+
private class RootJsonWriteExpression extends AggregateJsonWriteExpression
246264
implements WriteExpressionRenderer {
247265
private final boolean nullable;
248266
private final String path;

hibernate-core/src/main/java/org/hibernate/query/sqm/CastType.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public enum CastType {
3434
INTEGER, LONG, FLOAT, DOUBLE, FIXED,
3535
DATE, TIME, TIMESTAMP,
3636
OFFSET_TIMESTAMP, ZONE_TIMESTAMP,
37+
JSON,
3738
NULL,
3839
OTHER;
3940

hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/JdbcType.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,9 @@ static CastType getCastType(int typeCode) {
347347
return CastType.TIMESTAMP;
348348
case TIMESTAMP_WITH_TIMEZONE:
349349
return CastType.OFFSET_TIMESTAMP;
350+
case JSON:
351+
case JSON_ARRAY:
352+
return CastType.JSON;
350353
case NULL:
351354
return CastType.NULL;
352355
default:

0 commit comments

Comments
 (0)