Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 76a9fba

Browse files
committedApr 18, 2025·
Add GaussDB support for spring data jdbc
Signed-off-by: liubao68 <bismy@qq.com>
1 parent 1f2e694 commit 76a9fba

File tree

33 files changed

+1511
-15
lines changed

33 files changed

+1511
-15
lines changed
 

‎pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
<mysql-connector-java.version>8.0.33</mysql-connector-java.version>
4141
<postgresql.version>42.7.4</postgresql.version>
4242
<oracle.version>23.7.0.25.01</oracle.version>
43+
<gaussdb.version>506.0.0.b058</gaussdb.version>
4344

4445
<!-- R2DBC driver dependencies-->
4546
<r2dbc-postgresql.version>1.0.7.RELEASE</r2dbc-postgresql.version>

‎spring-data-jdbc/pom.xml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@
137137
<optional>true</optional>
138138
</dependency>
139139

140+
<dependency>
141+
<groupId>com.huaweicloud.gaussdb</groupId>
142+
<artifactId>gaussdbjdbc</artifactId>
143+
<version>${gaussdb.version}</version>
144+
<optional>true</optional>
145+
</dependency>
146+
140147
<dependency>
141148
<groupId>org.mariadb.jdbc</groupId>
142149
<artifactId>mariadb-java-client</artifactId>
@@ -318,6 +325,37 @@
318325
</plugins>
319326
</build>
320327
</profile>
328+
<profile>
329+
<id>gaussdb</id>
330+
<build>
331+
<plugins>
332+
<plugin>
333+
<groupId>org.apache.maven.plugins</groupId>
334+
<artifactId>maven-failsafe-plugin</artifactId>
335+
<executions>
336+
<execution>
337+
<id>gaussdb-test</id>
338+
<phase>integration-test</phase>
339+
<goals>
340+
<goal>integration-test</goal>
341+
</goals>
342+
<configuration>
343+
<includes>
344+
<include>**/*IntegrationTests.java</include>
345+
</includes>
346+
<excludes>
347+
<exclude/>
348+
</excludes>
349+
<systemPropertyVariables>
350+
<spring.profiles.active>gaussdb</spring.profiles.active>
351+
</systemPropertyVariables>
352+
</configuration>
353+
</execution>
354+
</executions>
355+
</plugin>
356+
</plugins>
357+
</build>
358+
</profile>
321359
<profile>
322360
<id>all-dbs</id>
323361
<build>
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2021-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.dialect;
17+
18+
import org.springframework.data.relational.core.dialect.GaussDBDialect;
19+
import org.springframework.util.ClassUtils;
20+
21+
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.HashSet;
24+
import java.util.List;
25+
import java.util.Set;
26+
import java.util.function.Consumer;
27+
28+
/**
29+
* JDBC specific GaussDBDialect Dialect.
30+
*
31+
* Notes: this file is token from JdbcPostgresDialect and add specific changes for GaussDB
32+
*
33+
* @author liubao
34+
*/
35+
public class JdbcGaussDBDialect extends GaussDBDialect {
36+
37+
public static final JdbcGaussDBDialect INSTANCE = new JdbcGaussDBDialect();
38+
39+
private static final Set<Class<?>> SIMPLE_TYPES;
40+
41+
static {
42+
43+
Set<Class<?>> simpleTypes = new HashSet<>(GaussDBDialect.INSTANCE.simpleTypes());
44+
List<String> simpleTypeNames = Arrays.asList( //
45+
"com.huawei.gaussdb.jdbc.util.PGobject", //
46+
"com.huawei.gaussdb.jdbc.geometric.PGpoint", //
47+
"com.huawei.gaussdb.jdbc.geometric.PGbox", //
48+
"com.huawei.gaussdb.jdbc.geometric.PGcircle", //
49+
"com.huawei.gaussdb.jdbc.geometric.PGline", //
50+
"com.huawei.gaussdb.jdbc.geometric.PGpath", //
51+
"com.huawei.gaussdb.jdbc.geometric.PGpolygon", //
52+
"com.huawei.gaussdb.jdbc.geometric.PGlseg" //
53+
);
54+
simpleTypeNames.forEach(name -> ifClassPresent(name, simpleTypes::add));
55+
SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
56+
}
57+
58+
@Override
59+
public Set<Class<?>> simpleTypes() {
60+
return SIMPLE_TYPES;
61+
}
62+
63+
/**
64+
* If the class is present on the class path, invoke the specified consumer {@code action} with the class object,
65+
* otherwise do nothing.
66+
*
67+
* @param action block to be executed if a value is present.
68+
*/
69+
private static void ifClassPresent(String className, Consumer<Class<?>> action) {
70+
if (ClassUtils.isPresent(className, GaussDBDialect.class.getClassLoader())) {
71+
action.accept(ClassUtils.resolveClassName(className, GaussDBDialect.class.getClassLoader()));
72+
}
73+
}
74+
}

‎spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,12 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.config;
1717

18-
import java.sql.Connection;
19-
import java.sql.DatabaseMetaData;
20-
import java.sql.SQLException;
21-
import java.util.List;
22-
import java.util.Locale;
23-
import java.util.Optional;
24-
25-
import javax.sql.DataSource;
26-
2718
import org.apache.commons.logging.Log;
2819
import org.apache.commons.logging.LogFactory;
2920
import org.springframework.core.io.support.SpringFactoriesLoader;
3021
import org.springframework.dao.NonTransientDataAccessException;
3122
import org.springframework.data.jdbc.core.dialect.JdbcDb2Dialect;
23+
import org.springframework.data.jdbc.core.dialect.JdbcGaussDBDialect;
3224
import org.springframework.data.jdbc.core.dialect.JdbcMySqlDialect;
3325
import org.springframework.data.jdbc.core.dialect.JdbcPostgresDialect;
3426
import org.springframework.data.jdbc.core.dialect.JdbcSqlServerDialect;
@@ -44,6 +36,14 @@
4436
import org.springframework.lang.Nullable;
4537
import org.springframework.util.StringUtils;
4638

39+
import javax.sql.DataSource;
40+
import java.sql.Connection;
41+
import java.sql.DatabaseMetaData;
42+
import java.sql.SQLException;
43+
import java.util.List;
44+
import java.util.Locale;
45+
import java.util.Optional;
46+
4747
/**
4848
* Resolves a {@link Dialect}. Resolution typically uses {@link JdbcOperations} to obtain and inspect a
4949
* {@link Connection}. Dialect resolution uses Spring's {@link SpringFactoriesLoader spring.factories} to determine
@@ -139,7 +139,9 @@ private static Dialect getDialect(Connection connection) throws SQLException {
139139
if (name.contains("oracle")) {
140140
return OracleDialect.INSTANCE;
141141
}
142-
142+
if (name.contains("gaussdb")) {
143+
return JdbcGaussDBDialect.INSTANCE;
144+
}
143145
LOG.info(String.format("Couldn't determine Dialect for \"%s\"", name));
144146
return null;
145147
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright 2021-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.dialect;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import java.sql.SQLException;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.List;
24+
import java.util.Objects;
25+
import java.util.Optional;
26+
27+
import com.huawei.gaussdb.jdbc.util.PGobject;
28+
import org.junit.jupiter.api.Test;
29+
import org.springframework.beans.factory.annotation.Autowired;
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.ComponentScan;
32+
import org.springframework.context.annotation.Configuration;
33+
import org.springframework.context.annotation.FilterType;
34+
import org.springframework.context.annotation.Import;
35+
import org.springframework.core.convert.converter.Converter;
36+
import org.springframework.data.annotation.Id;
37+
import org.springframework.data.convert.CustomConversions;
38+
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
39+
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
40+
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
41+
import org.springframework.data.jdbc.testing.DatabaseType;
42+
import org.springframework.data.jdbc.testing.EnabledOnDatabase;
43+
import org.springframework.data.jdbc.testing.IntegrationTest;
44+
import org.springframework.data.jdbc.testing.TestConfiguration;
45+
import org.springframework.data.mapping.model.SimpleTypeHolder;
46+
import org.springframework.data.relational.core.dialect.Dialect;
47+
import org.springframework.data.relational.core.mapping.Table;
48+
import org.springframework.data.repository.CrudRepository;
49+
50+
/**
51+
* Integration tests for GaussDB Dialect. Start this test with {@code -Dspring.profiles.active=gaussdb}.
52+
*
53+
* Notes: this file is token from PostgresDialectIntegrationTests and add specific changes for GaussDB
54+
*
55+
* @author liubao
56+
*/
57+
@IntegrationTest
58+
@EnabledOnDatabase(DatabaseType.GAUSSDB)
59+
public class GaussDBDialectIntegrationTests {
60+
61+
@Autowired CustomerRepository customerRepository;
62+
63+
@Test
64+
void shouldSaveAndLoadJson() throws SQLException {
65+
66+
PGobject sessionData = new PGobject();
67+
sessionData.setType("jsonb");
68+
sessionData.setValue("{\"hello\": \"json\"}");
69+
70+
Customer saved = customerRepository
71+
.save(new Customer(null, "Adam Smith", new JsonHolder("{\"hello\": \"world\"}"), sessionData));
72+
73+
Optional<Customer> loaded = customerRepository.findById(saved.getId());
74+
75+
assertThat(loaded).hasValueSatisfying(actual -> {
76+
77+
assertThat(actual.getPersonData().getContent()).isEqualTo("{\"hello\": \"world\"}");
78+
assertThat(actual.getSessionData().getValue()).isEqualTo("{\"hello\": \"json\"}");
79+
});
80+
}
81+
82+
@Configuration
83+
@Import(TestConfiguration.class)
84+
@EnableJdbcRepositories(considerNestedRepositories = true,
85+
includeFilters = @ComponentScan.Filter(value = CustomerRepository.class, type = FilterType.ASSIGNABLE_TYPE))
86+
static class Config {
87+
88+
@Bean
89+
CustomConversions jdbcCustomConversions(Dialect dialect) {
90+
SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER);
91+
92+
return new JdbcCustomConversions(
93+
CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), userConverters());
94+
}
95+
96+
private List<Object> storeConverters(Dialect dialect) {
97+
98+
List<Object> converters = new ArrayList<>();
99+
converters.addAll(dialect.getConverters());
100+
converters.addAll(JdbcCustomConversions.storeConverters());
101+
return converters;
102+
}
103+
104+
private List<Object> userConverters() {
105+
return Arrays.asList(JsonHolderToPGobjectConverter.INSTANCE, PGobjectToJsonHolderConverter.INSTANCE);
106+
}
107+
}
108+
109+
enum JsonHolderToPGobjectConverter implements Converter<JsonHolder, PGobject> {
110+
111+
INSTANCE;
112+
113+
@Override
114+
public PGobject convert(JsonHolder source) {
115+
PGobject result = new PGobject();
116+
result.setType("jsonb");
117+
try {
118+
result.setValue(source.getContent());
119+
} catch (SQLException e) {
120+
throw new RuntimeException(e);
121+
}
122+
return result;
123+
}
124+
}
125+
126+
enum PGobjectToJsonHolderConverter implements Converter<PGobject, JsonHolder> {
127+
128+
INSTANCE;
129+
130+
@Override
131+
public JsonHolder convert(PGobject source) {
132+
return new JsonHolder(source.getValue());
133+
}
134+
}
135+
136+
@Table("customers")
137+
public static final class Customer {
138+
139+
@Id private final Long id;
140+
private final String name;
141+
private final JsonHolder personData;
142+
private final PGobject sessionData;
143+
144+
public Customer(Long id, String name, JsonHolder personData, PGobject sessionData) {
145+
this.id = id;
146+
this.name = name;
147+
this.personData = personData;
148+
this.sessionData = sessionData;
149+
}
150+
151+
public Long getId() {
152+
return this.id;
153+
}
154+
155+
public String getName() {
156+
return this.name;
157+
}
158+
159+
public JsonHolder getPersonData() {
160+
return this.personData;
161+
}
162+
163+
public PGobject getSessionData() {
164+
return this.sessionData;
165+
}
166+
167+
public boolean equals(final Object o) {
168+
if (o == this)
169+
return true;
170+
if (!(o instanceof final Customer other))
171+
return false;
172+
final Object this$id = this.getId();
173+
final Object other$id = other.getId();
174+
if (!Objects.equals(this$id, other$id))
175+
return false;
176+
final Object this$name = this.getName();
177+
final Object other$name = other.getName();
178+
if (!Objects.equals(this$name, other$name))
179+
return false;
180+
final Object this$personData = this.getPersonData();
181+
final Object other$personData = other.getPersonData();
182+
if (!Objects.equals(this$personData, other$personData))
183+
return false;
184+
final Object this$sessionData = this.getSessionData();
185+
final Object other$sessionData = other.getSessionData();
186+
return Objects.equals(this$sessionData, other$sessionData);
187+
}
188+
189+
public int hashCode() {
190+
final int PRIME = 59;
191+
int result = 1;
192+
final Object $id = this.getId();
193+
result = result * PRIME + ($id == null ? 43 : $id.hashCode());
194+
final Object $name = this.getName();
195+
result = result * PRIME + ($name == null ? 43 : $name.hashCode());
196+
final Object $personData = this.getPersonData();
197+
result = result * PRIME + ($personData == null ? 43 : $personData.hashCode());
198+
final Object $sessionData = this.getSessionData();
199+
result = result * PRIME + ($sessionData == null ? 43 : $sessionData.hashCode());
200+
return result;
201+
}
202+
203+
public String toString() {
204+
return "PostgresDialectIntegrationTests.Customer(id=" + this.getId() + ", name=" + this.getName()
205+
+ ", personData=" + this.getPersonData() + ", sessionData=" + this.getSessionData() + ")";
206+
}
207+
}
208+
209+
public static class JsonHolder {
210+
String content;
211+
212+
public JsonHolder(String content) {
213+
this.content = content;
214+
}
215+
216+
public JsonHolder() {}
217+
218+
public String getContent() {
219+
return this.content;
220+
}
221+
222+
public void setContent(String content) {
223+
this.content = content;
224+
}
225+
}
226+
227+
interface CustomerRepository extends CrudRepository<Customer, Long> {}
228+
229+
}

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/DatabaseType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
*/
2626
public enum DatabaseType {
2727

28-
DB2, HSQL, H2, MARIADB, MYSQL, ORACLE, POSTGRES, SQL_SERVER("mssql");
28+
DB2, HSQL, H2, MARIADB, MYSQL, ORACLE, POSTGRES, SQL_SERVER("mssql"), GAUSSDB;
2929

3030
private final String profile;
3131

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2019-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.testing;
17+
18+
import org.testcontainers.containers.JdbcDatabaseContainer;
19+
import org.testcontainers.containers.wait.strategy.Wait;
20+
import org.testcontainers.containers.wait.strategy.WaitStrategy;
21+
import org.testcontainers.containers.wait.strategy.WaitStrategyTarget;
22+
import org.testcontainers.utility.DockerImageName;
23+
24+
import java.time.Duration;
25+
import java.util.concurrent.TimeUnit;
26+
27+
/**
28+
* Testcontainers implementation for GaussDB.
29+
*
30+
* Exposed ports: 8000
31+
*/
32+
public class GaussDBContainer<SELF extends GaussDBContainer<SELF>> extends JdbcDatabaseContainer<SELF> {
33+
34+
public static final String NAME = "gaussdb";
35+
36+
public static final String IMAGE = "opengauss/opengauss";
37+
38+
public static final String DEFAULT_TAG = "latest";
39+
40+
private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("opengauss/opengauss")
41+
.asCompatibleSubstituteFor("gaussdb");
42+
43+
public static final Integer GaussDB_PORT = 8000;
44+
45+
public static final String DEFAULT_USER_NAME = "r2dbc_test";
46+
47+
public static final String DEFAULT_PASSWORD = "R2dbc_test@12";
48+
49+
private String databaseName = "postgres";
50+
51+
private String username = DEFAULT_USER_NAME;
52+
53+
private String password = DEFAULT_PASSWORD;
54+
55+
public GaussDBContainer() {
56+
this(DEFAULT_IMAGE_NAME.withTag(DEFAULT_TAG));
57+
}
58+
59+
public GaussDBContainer(final String dockerImageName) {
60+
this(DockerImageName.parse(dockerImageName));
61+
}
62+
63+
public GaussDBContainer(final DockerImageName dockerImageName) {
64+
super(dockerImageName);
65+
setWaitStrategy(new WaitStrategy() {
66+
@Override
67+
public void waitUntilReady(WaitStrategyTarget waitStrategyTarget) {
68+
Wait.forListeningPort().waitUntilReady(waitStrategyTarget);
69+
try {
70+
// Open Gauss will set up users and password when ports are ready.
71+
Wait.forLogMessage(".*gs_ctl stopped.*", 1).waitUntilReady(waitStrategyTarget);
72+
// Not enough and no idea
73+
TimeUnit.SECONDS.sleep(3);
74+
} catch (InterruptedException e) {
75+
throw new RuntimeException(e);
76+
}
77+
}
78+
79+
@Override
80+
public WaitStrategy withStartupTimeout(Duration duration) {
81+
return Wait.forListeningPort().withStartupTimeout(duration);
82+
}
83+
});
84+
}
85+
86+
@Override
87+
protected void configure() {
88+
// Disable Postgres driver use of java.util.logging to reduce noise at startup time
89+
withUrlParam("loggerLevel", "OFF");
90+
withDatabaseName(databaseName);
91+
addExposedPorts(GaussDB_PORT);
92+
addFixedExposedPort(GaussDB_PORT, GaussDB_PORT);
93+
addEnv("GS_PORT", String.valueOf(GaussDB_PORT));
94+
addEnv("GS_USERNAME", username);
95+
addEnv("GS_PASSWORD", password);
96+
}
97+
98+
@Override
99+
public String getDriverClassName() {
100+
return "com.huawei.gaussdb.jdbc.Driver";
101+
}
102+
103+
@Override
104+
public String getJdbcUrl() {
105+
String additionalUrlParams = constructUrlParameters("?", "&");
106+
return (
107+
"jdbc:gaussdb://" +
108+
getHost() +
109+
":" +
110+
getMappedPort(GaussDB_PORT) +
111+
"/" +
112+
databaseName +
113+
additionalUrlParams
114+
);
115+
}
116+
117+
@Override
118+
public String getDatabaseName() {
119+
return databaseName;
120+
}
121+
122+
@Override
123+
public String getUsername() {
124+
return username;
125+
}
126+
127+
@Override
128+
public String getPassword() {
129+
return password;
130+
}
131+
132+
@Override
133+
public String getTestQueryString() {
134+
return "SELECT 1";
135+
}
136+
137+
@Override
138+
public SELF withDatabaseName(final String databaseName) {
139+
this.databaseName = databaseName;
140+
return self();
141+
}
142+
143+
@Override
144+
public SELF withUsername(final String username) {
145+
this.username = username;
146+
return self();
147+
}
148+
149+
@Override
150+
public SELF withPassword(final String password) {
151+
this.password = password;
152+
return self();
153+
}
154+
155+
@Override
156+
protected void waitUntilContainerStarted() {
157+
getWaitStrategy().waitUntilReady(this);
158+
}
159+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2019-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.testing;
17+
18+
import org.testcontainers.containers.JdbcDatabaseContainer;
19+
import org.testcontainers.containers.JdbcDatabaseContainerProvider;
20+
import org.testcontainers.jdbc.ConnectionUrl;
21+
import org.testcontainers.utility.DockerImageName;
22+
23+
/**
24+
* Factory for GaussDB containers.
25+
*/
26+
@SuppressWarnings("rawtypes")
27+
public class GaussDBContainerProvider extends JdbcDatabaseContainerProvider {
28+
29+
public static final String USER_PARAM = "user";
30+
31+
public static final String PASSWORD_PARAM = "password";
32+
33+
@Override
34+
public boolean supports(String databaseType) {
35+
return databaseType.equals(GaussDBContainer.NAME);
36+
}
37+
38+
@Override
39+
public JdbcDatabaseContainer newInstance() {
40+
return newInstance(GaussDBContainer.DEFAULT_TAG);
41+
}
42+
43+
@Override
44+
public JdbcDatabaseContainer newInstance(String tag) {
45+
return new GaussDBContainer(DockerImageName.parse(GaussDBContainer.IMAGE).withTag(tag));
46+
}
47+
48+
@Override
49+
public JdbcDatabaseContainer newInstance(ConnectionUrl connectionUrl) {
50+
return newInstanceFromConnectionUrl(connectionUrl, USER_PARAM, PASSWORD_PARAM);
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2017-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.data.jdbc.testing;
18+
19+
import com.huawei.gaussdb.jdbc.ds.PGSimpleDataSource;
20+
import com.huawei.gaussdb.jdbc.util.PSQLException;
21+
import org.springframework.context.annotation.Configuration;
22+
import org.springframework.core.env.Environment;
23+
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
24+
25+
import javax.sql.DataSource;
26+
27+
/**
28+
* {@link DataSource} setup for GaussDB. Starts a docker container with a GaussDB database.
29+
* <p>
30+
* Notes: this file is token from PostgresDataSourceConfiguration and add specific changes for GaussDB
31+
*
32+
* @author liubao
33+
*/
34+
@Configuration(proxyBeanMethods = false)
35+
@ConditionalOnDatabase(DatabaseType.GAUSSDB)
36+
public class GaussDBDataSourceConfiguration extends DataSourceConfiguration {
37+
38+
private static GaussDBContainer<?> GAUSSDB_CONTAINER;
39+
40+
public GaussDBDataSourceConfiguration(TestClass testClass, Environment environment) {
41+
super(testClass, environment);
42+
}
43+
44+
@Override
45+
protected DataSource createDataSource() {
46+
47+
if (GAUSSDB_CONTAINER == null) {
48+
49+
GaussDBContainer<?> container = new GaussDBContainer<>();
50+
container.start();
51+
52+
GAUSSDB_CONTAINER = container;
53+
}
54+
55+
try {
56+
PGSimpleDataSource dataSource = new PGSimpleDataSource();
57+
dataSource.setUrl(GAUSSDB_CONTAINER.getJdbcUrl());
58+
dataSource.setUser(GAUSSDB_CONTAINER.getUsername());
59+
dataSource.setPassword(GAUSSDB_CONTAINER.getPassword());
60+
61+
return dataSource;
62+
} catch (PSQLException e) {
63+
throw new RuntimeException(e);
64+
}
65+
}
66+
67+
@Override
68+
protected void customizePopulator(ResourceDatabasePopulator populator) {
69+
populator.setIgnoreFailedDrops(true);
70+
}
71+
72+
}

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* @author Oliver Gierke
3131
*/
3232
@Configuration(proxyBeanMethods = false)
33-
@Profile({ "hsql", "!h2 && !mysql && !mariadb && !postgres && !oracle && !db2 && !mssql" })
33+
@Profile({ "hsql", "!h2 && !mysql && !mariadb && !postgres && !oracle && !db2 && !mssql && !gaussdb" })
3434
class HsqlDataSourceConfiguration {
3535

3636
private final TestClass testClass;

‎spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestDatabaseFeatures.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ private void supportsGeneratedIdsInReferencedEntities() {
6262

6363
private void supportsArrays() {
6464

65-
assumeThat(database).isNotIn(Database.MySql, Database.MariaDb, Database.SqlServer, Database.Db2, Database.Oracle);
65+
assumeThat(database).isNotIn(Database.MySql, Database.MariaDb, Database.SqlServer, Database.Db2, Database.Oracle, Database.GaussDB);
6666
}
6767

6868
private void supportsNanosecondPrecision() {
6969

70-
assumeThat(database).isNotIn(Database.MySql, Database.PostgreSql, Database.MariaDb, Database.SqlServer);
70+
assumeThat(database).isNotIn(Database.MySql, Database.PostgreSql, Database.MariaDb, Database.SqlServer, Database.GaussDB);
7171
}
7272

7373
private void supportsMultiDimensionalArrays() {
@@ -93,7 +93,7 @@ public void databaseIs(Database database) {
9393
}
9494

9595
public enum Database {
96-
Hsql, H2, MySql, MariaDb, PostgreSql, SqlServer("microsoft"), Db2, Oracle;
96+
Hsql, H2, MySql, MariaDb, PostgreSql, SqlServer("microsoft"), Db2, Oracle, GaussDB;
9797

9898
private final String identification;
9999

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
DROP TABLE customers;
2+
3+
CREATE TABLE customers (
4+
id BIGSERIAL PRIMARY KEY,
5+
name TEXT NOT NULL,
6+
person_data JSONB,
7+
session_data JSONB
8+
);

‎spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core/JdbcAggregateTemplateIntegrationTests-gaussdb.sql

Lines changed: 476 additions & 0 deletions
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
CREATE SCHEMA OTHER;
2+
3+
CREATE TABLE OTHER.DUMMY_ENTITY
4+
(
5+
ID SERIAL PRIMARY KEY,
6+
NAME VARCHAR(30)
7+
);
8+
9+
CREATE TABLE OTHER.REFERENCED
10+
(
11+
DUMMY_ENTITY INTEGER,
12+
NAME VARCHAR(30)
13+
);
14+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE Dummy_Entity
2+
CREATE TABLE Dummy_Entity ( id SERIAL PRIMARY KEY)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE Dummy_Entity
2+
CREATE TABLE Dummy_Entity ( id SERIAL PRIMARY KEY)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TABLE dummy_entity;
2+
DROP TABLE element;
3+
CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
4+
CREATE TABLE element (id SERIAL PRIMARY KEY, content BIGINT, Dummy_Entity_key BIGINT,dummy_entity BIGINT);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
2+
CREATE TABLE OTHER_ENTITY ( ID SERIAL PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE dummy_entity;
2+
CREATE TABLE dummy_entity (id SERIAL PRIMARY KEY, PREFIX_ATTR1 BIGINT, PREFIX_ATTR2 VARCHAR(100));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
DROP TABLE dummy_entity;
2+
CREATE TABLE dummy_entity
3+
(
4+
"ID" SERIAL PRIMARY KEY,
5+
TEST VARCHAR(100)
6+
);
7+
DROP TABLE dummy_entity2;
8+
CREATE TABLE dummy_entity2
9+
(
10+
"ID" INTEGER PRIMARY KEY,
11+
TEST VARCHAR(100),
12+
PREFIX_ATTR BIGINT
13+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
DROP TABLE dummy_entity;
2+
CREATE TABLE dummy_entity
3+
(
4+
"ID" SERIAL PRIMARY KEY,
5+
TEST VARCHAR(100),
6+
PREFIX_TEST VARCHAR(100)
7+
);
8+
DROP TABLE dummy_entity2;
9+
CREATE TABLE dummy_entity2
10+
(
11+
"DUMMY_ID" BIGINT,
12+
"ORDER_KEY" BIGINT,
13+
TEST VARCHAR(100),
14+
PRIMARY KEY ("DUMMY_ID", "ORDER_KEY")
15+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
DROP TABLE dummy_entity;
2+
CREATE TABLE dummy_entity
3+
(
4+
"ID" SERIAL PRIMARY KEY,
5+
TEST VARCHAR(100),
6+
PREFIX_TEST VARCHAR(100)
7+
);
8+
DROP TABLE dummy_entity2;
9+
CREATE TABLE dummy_entity2
10+
(
11+
"ID" SERIAL PRIMARY KEY,
12+
TEST VARCHAR(100)
13+
);
14+
--
15+
-- SELECT "dummy_entity"."ID" AS "ID",
16+
-- "dummy_entity"."test" AS "test",
17+
-- "dummy_entity"."prefix_test" AS "prefix_test",
18+
-- "PREFIX_dummyEntity2"."id" AS "prefix_dummyentity2_id",
19+
-- "PREFIX_dummyEntity2"."test" AS "prefix_dummyentity2_test"
20+
-- FROM "dummy_entity"
21+
-- LEFT OUTER JOIN "dummy_entity2" AS "PREFIX_dummyEntity2" ON
22+
-- "PREFIX_dummyEntity2"."ID" = "dummy_entity"."ID"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
DROP TABLE ReadOnlyIdEntity;
2+
DROP TABLE PrimitiveIdEntity;
3+
DROP TABLE ImmutableWithManualIdentity;
4+
DROP TABLE EntityWithSeq;
5+
DROP TABLE PersistableEntityWithSeq;
6+
DROP TABLE PrimitiveIdEntityWithSeq;
7+
8+
CREATE TABLE ReadOnlyIdEntity (ID SERIAL PRIMARY KEY, NAME VARCHAR(100));
9+
CREATE TABLE PrimitiveIdEntity (ID SERIAL PRIMARY KEY, NAME VARCHAR(100));
10+
CREATE TABLE ImmutableWithManualIdentity (ID BIGINT PRIMARY KEY, NAME VARCHAR(100));
11+
CREATE TABLE SimpleSeq (ID BIGINT NOT NULL PRIMARY KEY, NAME VARCHAR(100));
12+
CREATE SEQUENCE simple_seq_seq START WITH 1;
13+
CREATE TABLE PersistableSeq (ID BIGINT NOT NULL PRIMARY KEY, NAME VARCHAR(100));
14+
CREATE SEQUENCE persistable_seq_seq START WITH 1;
15+
CREATE TABLE PrimitiveIdSeq (ID BIGINT NOT NULL PRIMARY KEY, NAME VARCHAR(100));
16+
CREATE SEQUENCE primitive_seq_seq START WITH 1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE dummy_entity;
2+
CREATE TABLE dummy_entity (id_Prop INTEGER PRIMARY KEY, NAME VARCHAR(100));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
DROP TABLE dummy_entity;
2+
DROP TABLE ROOT;
3+
DROP TABLE INTERMEDIATE;
4+
DROP TABLE LEAF;
5+
DROP TABLE WITH_DELIMITED_COLUMN;
6+
DROP TABLE ENTITY_WITH_SEQUENCE;
7+
DROP SEQUENCE ENTITY_SEQUENCE;
8+
9+
CREATE TABLE dummy_entity
10+
(
11+
id_Prop SERIAL PRIMARY KEY,
12+
NAME VARCHAR(100),
13+
POINT_IN_TIME TIMESTAMP,
14+
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
15+
FLAG BOOLEAN,
16+
REF BIGINT,
17+
DIRECTION VARCHAR(100),
18+
BYTES BYTEA
19+
);
20+
21+
CREATE TABLE ROOT
22+
(
23+
ID SERIAL PRIMARY KEY,
24+
NAME VARCHAR(100)
25+
);
26+
27+
CREATE TABLE INTERMEDIATE
28+
(
29+
ID SERIAL PRIMARY KEY,
30+
NAME VARCHAR(100),
31+
ROOT BIGINT,
32+
"ROOT_ID" BIGINT,
33+
"ROOT_KEY" INTEGER
34+
);
35+
36+
CREATE TABLE LEAF
37+
(
38+
ID SERIAL PRIMARY KEY,
39+
NAME VARCHAR(100),
40+
INTERMEDIATE BIGINT,
41+
"INTERMEDIATE_ID" BIGINT,
42+
"INTERMEDIATE_KEY" INTEGER
43+
);
44+
45+
CREATE TABLE "WITH_DELIMITED_COLUMN"
46+
(
47+
ID SERIAL PRIMARY KEY,
48+
"ORG.XTUNIT.IDENTIFIER" VARCHAR(100),
49+
"STYPE" VARCHAR(100)
50+
);
51+
52+
CREATE TABLE ENTITY_WITH_SEQUENCE
53+
(
54+
ID BIGINT,
55+
NAME VARCHAR(100)
56+
);
57+
58+
CREATE SEQUENCE "ENTITY_SEQUENCE" START WITH 1 INCREMENT BY 1 NO MAXVALUE;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DROP TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION;
2+
DROP TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS;
3+
4+
CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS ( id_Timestamp TIMESTAMP PRIMARY KEY, bool boolean, SOME_ENUM VARCHAR(100), big_Decimal DECIMAL(65), big_Integer BIGINT, date TIMESTAMP, local_Date_Time TIMESTAMP, zoned_Date_Time VARCHAR(30));
5+
CREATE TABLE ENTITY_WITH_COLUMNS_REQUIRING_CONVERSIONS_RELATION ( "ID_TIMESTAMP" TIMESTAMP PRIMARY KEY, data VARCHAR(100));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DROP TABLE person;
2+
DROP TABLE address;
3+
CREATE TABLE person ( id SERIAL PRIMARY KEY, name VARCHAR(100));
4+
CREATE TABLE address ( id SERIAL PRIMARY KEY, street VARCHAR(100), person_id INT);
5+
ALTER TABLE address ADD FOREIGN KEY (person_id) REFERENCES person(id);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TABLE element;
2+
DROP TABLE dummy_entity;
3+
CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
4+
CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100), dummy_entity BIGINT);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TABLE element;
2+
DROP TABLE dummy_entity;
3+
CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
4+
CREATE TABLE element (content VARCHAR(100), dummy_entity BIGINT);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
DROP TABLE element;
2+
DROP TABLE dummy_entity;
3+
4+
DROP TABLE root;
5+
DROP TABLE intermediate;
6+
DROP TABLE leaf;
7+
8+
CREATE TABLE dummy_entity
9+
(
10+
id SERIAL PRIMARY KEY,
11+
NAME VARCHAR(100)
12+
);
13+
CREATE TABLE element
14+
(
15+
id SERIAL PRIMARY KEY,
16+
content VARCHAR(100),
17+
dummy_entity_key BIGINT,
18+
dummy_entity BIGINT
19+
);
20+
21+
CREATE TABLE root
22+
(
23+
id SERIAL PRIMARY KEY
24+
);
25+
CREATE TABLE intermediate
26+
(
27+
id SERIAL PRIMARY KEY,
28+
root BIGINT NOT NULL,
29+
root_key INTEGER NOT NULL
30+
);
31+
CREATE TABLE leaf
32+
(
33+
name VARCHAR(100),
34+
intermediate BIGINT NOT NULL,
35+
intermediate_key INTEGER NOT NULL
36+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
DROP TABLE element;
2+
DROP TABLE dummy_entity;
3+
CREATE TABLE dummy_entity ( id SERIAL PRIMARY KEY, NAME VARCHAR(100));
4+
CREATE TABLE element (id SERIAL PRIMARY KEY, content VARCHAR(100),dummy_entity_key VARCHAR(100), dummy_entity BIGINT);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP TABLE car;
2+
CREATE TABLE car ( id SERIAL PRIMARY KEY, model VARCHAR(100));
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright 2019-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.relational.core.dialect;
17+
18+
import java.net.InetAddress;
19+
import java.net.URI;
20+
import java.net.URL;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Map;
25+
import java.util.Set;
26+
import java.util.UUID;
27+
28+
import org.springframework.data.relational.core.sql.Functions;
29+
import org.springframework.data.relational.core.sql.IdentifierProcessing;
30+
import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing;
31+
import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting;
32+
import org.springframework.data.relational.core.sql.LockOptions;
33+
import org.springframework.data.relational.core.sql.SQL;
34+
import org.springframework.data.relational.core.sql.SimpleFunction;
35+
import org.springframework.data.relational.core.sql.SqlIdentifier;
36+
import org.springframework.data.relational.core.sql.TableLike;
37+
38+
/**
39+
* An SQL dialect for GaussDB.
40+
*
41+
* Notes: this file is token from PostgresDialect and add specific changes for GaussDB
42+
*
43+
* @author liubao
44+
*/
45+
public class GaussDBDialect extends AbstractDialect {
46+
47+
/**
48+
* Singleton instance.
49+
*/
50+
public static final GaussDBDialect INSTANCE = new GaussDBDialect();
51+
52+
private static final Set<Class<?>> POSTGRES_SIMPLE_TYPES = Set.of(UUID.class, URL.class, URI.class, InetAddress.class,
53+
Map.class);
54+
55+
private final IdentifierProcessing identifierProcessing = IdentifierProcessing.create(Quoting.ANSI,
56+
LetterCasing.LOWER_CASE);
57+
58+
private final IdGeneration idGeneration = new IdGeneration() {
59+
60+
@Override
61+
public String createSequenceQuery(SqlIdentifier sequenceName) {
62+
return "SELECT nextval('%s')".formatted(sequenceName.toSql(getIdentifierProcessing()));
63+
}
64+
};
65+
66+
protected GaussDBDialect() {}
67+
68+
private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
69+
70+
@Override
71+
public String getLimit(long limit) {
72+
return "LIMIT " + limit;
73+
}
74+
75+
@Override
76+
public String getOffset(long offset) {
77+
return "OFFSET " + offset;
78+
}
79+
80+
@Override
81+
public String getLimitOffset(long limit, long offset) {
82+
return String.format("LIMIT %d OFFSET %d", limit, offset);
83+
}
84+
85+
@Override
86+
public Position getClausePosition() {
87+
return Position.AFTER_ORDER_BY;
88+
}
89+
};
90+
91+
private static final ObjectArrayColumns ARRAY_COLUMNS = ObjectArrayColumns.INSTANCE;
92+
93+
@Override
94+
public LimitClause limit() {
95+
return LIMIT_CLAUSE;
96+
}
97+
98+
private final PostgresLockClause LOCK_CLAUSE = new PostgresLockClause(this.getIdentifierProcessing());
99+
100+
@Override
101+
public LockClause lock() {
102+
return LOCK_CLAUSE;
103+
}
104+
105+
@Override
106+
public ArrayColumns getArraySupport() {
107+
return ARRAY_COLUMNS;
108+
}
109+
110+
@Override
111+
public Collection<Object> getConverters() {
112+
return Collections.singletonList(TimestampAtUtcToOffsetDateTimeConverter.INSTANCE);
113+
}
114+
115+
static class PostgresLockClause implements LockClause {
116+
117+
private final IdentifierProcessing identifierProcessing;
118+
119+
PostgresLockClause(IdentifierProcessing identifierProcessing) {
120+
this.identifierProcessing = identifierProcessing;
121+
}
122+
123+
@Override
124+
public String getLock(LockOptions lockOptions) {
125+
126+
List<TableLike> tables = lockOptions.getFrom().getTables();
127+
if (tables.isEmpty()) {
128+
return "";
129+
}
130+
131+
// get the first table and obtain last part if the identifier is a composed one.
132+
SqlIdentifier identifier = tables.get(0).getName();
133+
SqlIdentifier last = identifier;
134+
135+
for (SqlIdentifier sqlIdentifier : identifier) {
136+
last = sqlIdentifier;
137+
}
138+
139+
// without schema
140+
String tableName = last.toSql(this.identifierProcessing);
141+
142+
return switch (lockOptions.getLockMode()) {
143+
case PESSIMISTIC_WRITE -> "FOR UPDATE OF " + tableName;
144+
case PESSIMISTIC_READ -> "FOR SHARE OF " + tableName;
145+
};
146+
}
147+
148+
@Override
149+
public Position getClausePosition() {
150+
return Position.AFTER_ORDER_BY;
151+
}
152+
}
153+
154+
@Override
155+
public IdentifierProcessing getIdentifierProcessing() {
156+
return identifierProcessing;
157+
}
158+
159+
@Override
160+
public Set<Class<?>> simpleTypes() {
161+
return POSTGRES_SIMPLE_TYPES;
162+
}
163+
164+
@Override
165+
public SimpleFunction getExistsFunction() {
166+
return Functions.least(Functions.count(SQL.literalOf(1)), SQL.literalOf(1));
167+
}
168+
169+
@Override
170+
public IdGeneration getIdGeneration() {
171+
return idGeneration;
172+
}
173+
}

0 commit comments

Comments
 (0)
Please sign in to comment.