diff --git a/pom.xml b/pom.xml
index 0b20365fcf..0053a81188 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,6 +92,7 @@
1.4.200
3.34.0
10.14.2.0
+ 2.9.12
5.15.14
2.3.1
2.3.0.1
diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml
index 8c93fc5c41..388a97f6b2 100644
--- a/spring-batch-core/pom.xml
+++ b/spring-batch-core/pom.xml
@@ -184,6 +184,12 @@
${derby.version}
test
+
+ com.sap.cloud.db.jdbc
+ ngdbc
+ ${hana.version}
+ test
+
commons-io
commons-io
diff --git a/spring-batch-core/src/main/resources/batch-hana.properties b/spring-batch-core/src/main/resources/batch-hana.properties
new file mode 100644
index 0000000000..2c1be01e24
--- /dev/null
+++ b/spring-batch-core/src/main/resources/batch-hana.properties
@@ -0,0 +1,17 @@
+# Placeholders batch.*
+# for SAP HANA:
+batch.jdbc.driver=com.sap.db.jdbc.Driver
+batch.jdbc.url=jdbc:sap://localhost:39015/
+batch.jdbc.user=SPRING_TEST
+batch.jdbc.password=Spr1ng_test
+batch.database.incrementer.class=org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer
+batch.schema.script=classpath:/org/springframework/batch/core/schema-hana.sql
+batch.drop.script=classpath:/org/springframework/batch/core/schema-drop-hana.sql
+batch.jdbc.testWhileIdle=true
+batch.jdbc.validationQuery=
+
+
+# Non-platform dependent settings that you might like to change
+batch.data.source.init=true
+batch.table.prefix=BATCH_
+
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-hana.sql b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-hana.sql
new file mode 100644
index 0000000000..944db3ec39
--- /dev/null
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-drop-hana.sql
@@ -0,0 +1,11 @@
+-- Autogenerated: do not edit this file
+ DROP TABLE BATCH_STEP_EXECUTION_CONTEXT ;
+DROP TABLE BATCH_JOB_EXECUTION_CONTEXT ;
+DROP TABLE BATCH_JOB_EXECUTION_PARAMS ;
+DROP TABLE BATCH_STEP_EXECUTION ;
+DROP TABLE BATCH_JOB_EXECUTION ;
+DROP TABLE BATCH_JOB_INSTANCE ;
+
+DROP SEQUENCE BATCH_STEP_EXECUTION_SEQ ;
+DROP SEQUENCE BATCH_JOB_EXECUTION_SEQ ;
+DROP SEQUENCE BATCH_JOB_SEQ ;
diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-hana.sql b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-hana.sql
new file mode 100644
index 0000000000..b9f43b4120
--- /dev/null
+++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-hana.sql
@@ -0,0 +1,81 @@
+-- Autogenerated: do not edit this file
+
+CREATE TABLE BATCH_JOB_INSTANCE (
+ JOB_INSTANCE_ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ VERSION BIGINT ,
+ JOB_NAME VARCHAR(100) NOT NULL,
+ JOB_KEY VARCHAR(32) NOT NULL,
+ constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
+) ;
+
+CREATE TABLE BATCH_JOB_EXECUTION (
+ JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ VERSION BIGINT ,
+ JOB_INSTANCE_ID BIGINT NOT NULL,
+ CREATE_TIME TIMESTAMP NOT NULL,
+ START_TIME TIMESTAMP DEFAULT NULL ,
+ END_TIME TIMESTAMP DEFAULT NULL ,
+ STATUS VARCHAR(10) ,
+ EXIT_CODE VARCHAR(2500) ,
+ EXIT_MESSAGE VARCHAR(2500) ,
+ LAST_UPDATED TIMESTAMP,
+ JOB_CONFIGURATION_LOCATION VARCHAR(2500) ,
+ constraint JOB_INST_EXEC_FK foreign key (JOB_INSTANCE_ID)
+ references BATCH_JOB_INSTANCE(JOB_INSTANCE_ID)
+) ;
+
+CREATE TABLE BATCH_JOB_EXECUTION_PARAMS (
+ JOB_EXECUTION_ID BIGINT NOT NULL ,
+ TYPE_CD VARCHAR(6) NOT NULL ,
+ KEY_NAME VARCHAR(100) NOT NULL ,
+ STRING_VAL VARCHAR(250) ,
+ DATE_VAL TIMESTAMP DEFAULT NULL ,
+ LONG_VAL BIGINT ,
+ DOUBLE_VAL DOUBLE ,
+ IDENTIFYING VARCHAR(1) NOT NULL ,
+ constraint JOB_EXEC_PARAMS_FK foreign key (JOB_EXECUTION_ID)
+ references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
+) ;
+
+CREATE TABLE BATCH_STEP_EXECUTION (
+ STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ VERSION BIGINT NOT NULL,
+ STEP_NAME VARCHAR(100) NOT NULL,
+ JOB_EXECUTION_ID BIGINT NOT NULL,
+ START_TIME TIMESTAMP NOT NULL ,
+ END_TIME TIMESTAMP DEFAULT NULL ,
+ STATUS VARCHAR(10) ,
+ COMMIT_COUNT BIGINT ,
+ READ_COUNT BIGINT ,
+ FILTER_COUNT BIGINT ,
+ WRITE_COUNT BIGINT ,
+ READ_SKIP_COUNT BIGINT ,
+ WRITE_SKIP_COUNT BIGINT ,
+ PROCESS_SKIP_COUNT BIGINT ,
+ ROLLBACK_COUNT BIGINT ,
+ EXIT_CODE VARCHAR(2500) ,
+ EXIT_MESSAGE VARCHAR(2500) ,
+ LAST_UPDATED TIMESTAMP,
+ constraint JOB_EXEC_STEP_FK foreign key (JOB_EXECUTION_ID)
+ references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
+) ;
+
+CREATE TABLE BATCH_STEP_EXECUTION_CONTEXT (
+ STEP_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
+ SHORT_CONTEXT VARCHAR(2500) NOT NULL,
+ SERIALIZED_CONTEXT CLOB ,
+ constraint STEP_EXEC_CTX_FK foreign key (STEP_EXECUTION_ID)
+ references BATCH_STEP_EXECUTION(STEP_EXECUTION_ID)
+) ;
+
+CREATE TABLE BATCH_JOB_EXECUTION_CONTEXT (
+ JOB_EXECUTION_ID BIGINT NOT NULL PRIMARY KEY,
+ SHORT_CONTEXT VARCHAR(2500) NOT NULL,
+ SERIALIZED_CONTEXT CLOB ,
+ constraint JOB_EXEC_CTX_FK foreign key (JOB_EXECUTION_ID)
+ references BATCH_JOB_EXECUTION(JOB_EXECUTION_ID)
+) ;
+
+CREATE SEQUENCE BATCH_STEP_EXECUTION_SEQ START WITH 0 MINVALUE 0 NO CYCLE;
+CREATE SEQUENCE BATCH_JOB_EXECUTION_SEQ START WITH 0 MINVALUE 0 NO CYCLE;
+CREATE SEQUENCE BATCH_JOB_SEQ START WITH 0 MINVALUE 0 NO CYCLE;
diff --git a/spring-batch-core/src/main/sql/hana.properties b/spring-batch-core/src/main/sql/hana.properties
new file mode 100644
index 0000000000..313903c84d
--- /dev/null
+++ b/spring-batch-core/src/main/sql/hana.properties
@@ -0,0 +1,14 @@
+platform=hana
+# SQL language oddities
+BIGINT = BIGINT
+IDENTITY =
+GENERATED = GENERATED BY DEFAULT AS IDENTITY
+IFEXISTSBEFORE =
+DOUBLE = DOUBLE
+BLOB = BLOB
+CLOB = CLOB
+TIMESTAMP = TIMESTAMP
+VARCHAR = VARCHAR
+CHAR = VARCHAR
+# for generating drop statements...
+SEQUENCE = SEQUENCE
diff --git a/spring-batch-core/src/main/sql/hana.vpp b/spring-batch-core/src/main/sql/hana.vpp
new file mode 100644
index 0000000000..b570903459
--- /dev/null
+++ b/spring-batch-core/src/main/sql/hana.vpp
@@ -0,0 +1,3 @@
+#macro (sequence $name $value)CREATE SEQUENCE ${name} START WITH ${value} MINVALUE 0 NO CYCLE;
+#end
+#macro (notnull $name $type)ALTER (${name} ${type} NOT NULL)#end
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAContainer.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAContainer.java
new file mode 100644
index 0000000000..06fa8871a4
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAContainer.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.test.repository;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.testcontainers.containers.JdbcDatabaseContainer;
+import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.LicenseAcceptance;
+
+import com.github.dockerjava.api.model.Ulimit;
+
+/**
+ * @author Jonathan Bregler
+ */
+public class HANAContainer> extends JdbcDatabaseContainer {
+
+ private static final Integer PORT = 39041;
+
+ private static final String SYSTEM_USER = "SYSTEM";
+ private static final String SYSTEM_USER_PASSWORD = "HXEHana1";
+
+ public HANAContainer(DockerImageName image) {
+
+ super( image );
+
+ addExposedPorts( 39013, 39017, 39041, 39042, 39043, 39044, 39045, 1128, 1129, 59013, 59014 );
+
+ // create ulimits
+ Ulimit[] ulimits = new Ulimit[]{ new Ulimit( "nofile", 1048576L, 1048576L ) };
+
+ // create sysctls Map.
+ Map sysctls = new HashMap();
+
+ sysctls.put( "kernel.shmmax", "1073741824" );
+ sysctls.put( "net.ipv4.ip_local_port_range", "40000 60999" );
+
+ // Apply mounts, ulimits and sysctls.
+ this.withCreateContainerCmdModifier( it -> it.getHostConfig().withUlimits( ulimits ).withSysctls( sysctls ) );
+
+ // Arguments for Image.
+ this.withCommand( "--master-password " + SYSTEM_USER_PASSWORD + " --agree-to-sap-license" );
+
+ // Determine if container is ready.
+ this.waitStrategy = new LogMessageWaitStrategy().withRegEx( ".*Startup finished!*\\s" ).withTimes( 1 )
+ .withStartupTimeout( Duration.of( 600, ChronoUnit.SECONDS ) );
+ }
+
+ @Override
+ protected void configure() {
+ /*
+ * Enforce that the license is accepted - do not remove. License available at:
+ * https://www.sap.com/docs/download/cmp/2016/06/sap-hana-express-dev-agmt-and- exhibit.pdf
+ */
+
+ // If license was not accepted programmatically, check if it was accepted via
+ // resource file
+ if ( !getEnvMap().containsKey( "AGREE_TO_SAP_LICENSE" ) ) {
+ LicenseAcceptance.assertLicenseAccepted( this.getDockerImageName() );
+ acceptLicense();
+ }
+ }
+
+ /**
+ * Accepts the license for the SAP HANA Express container by setting the AGREE_TO_SAP_LICENSE=Y Calling this method
+ * will automatically accept the license at:
+ * https://www.sap.com/docs/download/cmp/2016/06/sap-hana-express-dev-agmt-and-exhibit.pdf
+ *
+ * @return The container itself with an environment variable accepting the SAP HANA Express license
+ */
+ public SELF acceptLicense() {
+ addEnv( "AGREE_TO_SAP_LICENSE", "Y" );
+ return self();
+ }
+
+ @Override
+ protected Set getLivenessCheckPorts() {
+ return new HashSet<>( Arrays.asList( new Integer[]{ getMappedPort( PORT ) } ) );
+ }
+
+ @Override
+ protected void waitUntilContainerStarted() {
+ getWaitStrategy().waitUntilReady( this );
+ }
+
+ @Override
+ public String getDriverClassName() {
+ return "com.sap.db.jdbc.Driver";
+ }
+
+ @Override
+ public String getUsername() {
+ return SYSTEM_USER;
+ }
+
+ @Override
+ public String getPassword() {
+ return SYSTEM_USER_PASSWORD;
+ }
+
+ @Override
+ public String getTestQueryString() {
+ return "SELECT 1 FROM SYS.DUMMY";
+ }
+
+ @Override
+ public String getJdbcUrl() {
+ return "jdbc:sap://" + getContainerIpAddress() + ":" + getMappedPort( PORT ) + "/";
+ }
+}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAJobRepositoryIntegrationTests.java
new file mode 100644
index 0000000000..276aaf39d7
--- /dev/null
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/HANAJobRepositoryIntegrationTests.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2020-2021 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.core.test.repository;
+
+import javax.sql.DataSource;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.batch.core.ExitStatus;
+import org.springframework.batch.core.Job;
+import org.springframework.batch.core.JobExecution;
+import org.springframework.batch.core.JobParameters;
+import org.springframework.batch.core.JobParametersBuilder;
+import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
+import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
+import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
+import org.springframework.batch.core.launch.JobLauncher;
+import org.springframework.batch.repeat.RepeatStatus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.testcontainers.utility.DockerImageName;
+
+import com.sap.db.jdbcext.HanaDataSource;
+
+/**
+ * @author Jonathan Bregler
+ */
+@RunWith(SpringRunner.class)
+@ContextConfiguration
+public class HANAJobRepositoryIntegrationTests {
+
+ private static final DockerImageName HANA_IMAGE = DockerImageName.parse( "store/saplabs/hanaexpress:2.00.054.00.20210603.1" );
+
+ @ClassRule
+ public static HANAContainer> hana = new HANAContainer<>( HANA_IMAGE ).acceptLicense();
+
+ @Autowired
+ private DataSource dataSource;
+ @Autowired
+ private JobLauncher jobLauncher;
+ @Autowired
+ private Job job;
+
+ @Before
+ public void setUp() {
+ ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
+ databasePopulator.addScript( new ClassPathResource( "/org/springframework/batch/core/schema-hana.sql" ) );
+ databasePopulator.execute( this.dataSource );
+ }
+
+ @Test
+ public void testJobExecution() throws Exception {
+ // given
+ JobParameters jobParameters = new JobParametersBuilder().toJobParameters();
+
+ // when
+ JobExecution jobExecution = this.jobLauncher.run( this.job, jobParameters );
+
+ // then
+ Assert.assertNotNull( jobExecution );
+ Assert.assertEquals( ExitStatus.COMPLETED, jobExecution.getExitStatus() );
+ }
+
+ @Configuration
+ @EnableBatchProcessing
+ static class TestConfiguration {
+
+ @Bean
+ public DataSource dataSource() throws Exception {
+ HanaDataSource dataSource = new HanaDataSource();
+ dataSource.setUser( hana.getUsername() );
+ dataSource.setPassword( hana.getPassword() );
+ dataSource.setUrl( hana.getJdbcUrl() );
+ return dataSource;
+ }
+
+ @Bean
+ public Job job(JobBuilderFactory jobs, StepBuilderFactory steps) {
+ return jobs.get( "job" )
+ .start( steps.get( "step" ).tasklet( (contribution, chunkContext) -> RepeatStatus.FINISHED ).build() )
+ .build();
+ }
+
+ }
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java
index b40fff16ff..f16f2be8c3 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilder.java
@@ -25,6 +25,7 @@
import org.springframework.batch.item.database.support.Db2PagingQueryProvider;
import org.springframework.batch.item.database.support.DerbyPagingQueryProvider;
import org.springframework.batch.item.database.support.H2PagingQueryProvider;
+import org.springframework.batch.item.database.support.HanaPagingQueryProvider;
import org.springframework.batch.item.database.support.HsqlPagingQueryProvider;
import org.springframework.batch.item.database.support.MySqlPagingQueryProvider;
import org.springframework.batch.item.database.support.OraclePagingQueryProvider;
@@ -359,6 +360,7 @@ private PagingQueryProvider determineQueryProvider(DataSource dataSource) {
case DB2ZOS:
case DB2AS400: provider = new Db2PagingQueryProvider(); break;
case H2: provider = new H2PagingQueryProvider(); break;
+ case HANA: provider = new HanaPagingQueryProvider(); break;
case HSQL: provider = new HsqlPagingQueryProvider(); break;
case SQLSERVER: provider = new SqlServerPagingQueryProvider(); break;
case MYSQL: provider = new MySqlPagingQueryProvider(); break;
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactory.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactory.java
index d95c6574b6..7a4b7e4de6 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactory.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactory.java
@@ -25,6 +25,7 @@
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.DerbyMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.H2SequenceMaxValueIncrementer;
+import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer;
@@ -37,6 +38,7 @@
import static org.springframework.batch.support.DatabaseType.DB2ZOS;
import static org.springframework.batch.support.DatabaseType.DERBY;
import static org.springframework.batch.support.DatabaseType.H2;
+import static org.springframework.batch.support.DatabaseType.HANA;
import static org.springframework.batch.support.DatabaseType.HSQL;
import static org.springframework.batch.support.DatabaseType.MYSQL;
import static org.springframework.batch.support.DatabaseType.ORACLE;
@@ -98,6 +100,9 @@ else if (databaseType == HSQL) {
else if (databaseType == H2) {
return new H2SequenceMaxValueIncrementer(dataSource, incrementerName);
}
+ else if (databaseType == HANA) {
+ return new HanaSequenceMaxValueIncrementer(dataSource, incrementerName);
+ }
else if (databaseType == MYSQL) {
MySQLMaxValueIncrementer mySQLMaxValueIncrementer = new MySQLMaxValueIncrementer(dataSource, incrementerName, incrementerColumnName);
mySQLMaxValueIncrementer.setUseNewConnection(true);
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HanaPagingQueryProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HanaPagingQueryProvider.java
new file mode 100644
index 0000000000..eabae383c1
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/HanaPagingQueryProvider.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2006-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.database.support;
+
+import org.springframework.batch.item.database.PagingQueryProvider;
+import org.springframework.util.StringUtils;
+
+/**
+ * SAP HANA implementation of a {@link PagingQueryProvider} using database specific features.
+ *
+ * @author Jonathan Bregler
+ * @since 4.0
+ */
+public class HanaPagingQueryProvider extends AbstractSqlPagingQueryProvider {
+
+ @Override
+ public String generateFirstPageQuery(int pageSize) {
+ return SqlPagingQueryUtils.generateLimitSqlQuery(this, false, buildLimitClause(pageSize));
+ }
+
+ @Override
+ public String generateRemainingPagesQuery(int pageSize) {
+ if(StringUtils.hasText(getGroupClause())) {
+ return SqlPagingQueryUtils.generateLimitGroupedSqlQuery(this, true, buildLimitClause(pageSize));
+ }
+ else {
+ return SqlPagingQueryUtils.generateLimitSqlQuery(this, true, buildLimitClause(pageSize));
+ }
+ }
+
+ private String buildLimitClause(int pageSize) {
+ return new StringBuilder().append("LIMIT ").append(pageSize).toString();
+ }
+
+ @Override
+ public String generateJumpToItemQuery(int itemIndex, int pageSize) {
+ int page = itemIndex / pageSize;
+ int offset = (page * pageSize) - 1;
+ offset = offset<0 ? 0 : offset;
+ String limitClause = new StringBuilder().append("LIMIT 1 OFFSET ").append(offset).toString();
+ return SqlPagingQueryUtils.generateLimitJumpToQuery(this, limitClause);
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryProviderFactoryBean.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryProviderFactoryBean.java
index dad72b3261..6c29b37542 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryProviderFactoryBean.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/support/SqlPagingQueryProviderFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2012 the original author or authors.
+ * Copyright 2006-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
import static org.springframework.batch.support.DatabaseType.DB2AS400;
import static org.springframework.batch.support.DatabaseType.DERBY;
import static org.springframework.batch.support.DatabaseType.H2;
+import static org.springframework.batch.support.DatabaseType.HANA;
import static org.springframework.batch.support.DatabaseType.HSQL;
import static org.springframework.batch.support.DatabaseType.MYSQL;
import static org.springframework.batch.support.DatabaseType.ORACLE;
@@ -78,6 +79,7 @@ public class SqlPagingQueryProviderFactoryBean implements FactoryBean nameMap;
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactoryTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactoryTests.java
index d69ba7db18..b2305d0428 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactoryTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/DefaultDataFieldMaxValueIncrementerFactoryTests.java
@@ -24,6 +24,7 @@
import org.springframework.jdbc.support.incrementer.Db2LuwMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.Db2MainframeMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.DerbyMaxValueIncrementer;
+import org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.HsqlMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer;
import org.springframework.jdbc.support.incrementer.OracleSequenceMaxValueIncrementer;
@@ -62,6 +63,7 @@ public void testSupportedDatabaseType(){
assertTrue(factory.isSupportedIncrementerType("sqlserver"));
assertTrue(factory.isSupportedIncrementerType("sybase"));
assertTrue(factory.isSupportedIncrementerType("sqlite"));
+ assertTrue(factory.isSupportedIncrementerType("hana"));
}
public void testUnsupportedDatabaseType(){
@@ -128,5 +130,9 @@ public void testSybase(){
public void testSqlite(){
assertTrue(factory.getIncrementer("sqlite", "NAME") instanceof SqliteMaxValueIncrementer);
}
+
+ public void testHana(){
+ assertTrue(factory.getIncrementer("hana", "NAME") instanceof HanaSequenceMaxValueIncrementer);
+ }
}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HanaPagingQueryProviderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HanaPagingQueryProviderTests.java
new file mode 100644
index 0000000000..5f1b836e90
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/HanaPagingQueryProviderTests.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2006-2018 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.item.database.support;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Test;
+
+import org.springframework.batch.item.database.Order;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * @author Jonathan Bregler
+ * @since 4.0
+ */
+public class HanaPagingQueryProviderTests extends AbstractSqlPagingQueryProviderTests {
+
+ public HanaPagingQueryProviderTests() {
+ pagingQueryProvider = new HanaPagingQueryProvider();
+ }
+
+ @Test
+ @Override
+ public void testGenerateFirstPageQuery() {
+ String sql = "SELECT id, name, age FROM foo WHERE bar = 1 ORDER BY id ASC LIMIT 100";
+ String s = pagingQueryProvider.generateFirstPageQuery(pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Test @Override
+ public void testGenerateRemainingPagesQuery() {
+ String sql = "SELECT id, name, age FROM foo WHERE (bar = 1) AND ((id > ?)) ORDER BY id ASC LIMIT 100";
+ String s = pagingQueryProvider.generateRemainingPagesQuery(pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Test @Override
+ public void testGenerateJumpToItemQuery() {
+ String sql = "SELECT id FROM foo WHERE bar = 1 ORDER BY id ASC LIMIT 1 OFFSET 99";
+ String s = pagingQueryProvider.generateJumpToItemQuery(145, pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Test @Override
+ public void testGenerateJumpToItemQueryForFirstPage() {
+ String sql = "SELECT id FROM foo WHERE bar = 1 ORDER BY id ASC LIMIT 1 OFFSET 0";
+ String s = pagingQueryProvider.generateJumpToItemQuery(45, pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Override
+ @Test
+ public void testGenerateFirstPageQueryWithGroupBy() {
+ pagingQueryProvider.setGroupClause("dep");
+ String sql = "SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep ORDER BY id ASC LIMIT 100";
+ String s = pagingQueryProvider.generateFirstPageQuery(pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Override
+ @Test
+ public void testGenerateRemainingPagesQueryWithGroupBy() {
+ pagingQueryProvider.setGroupClause("dep");
+ String sql = "SELECT * FROM (SELECT id, name, age FROM foo WHERE bar = 1 GROUP BY dep) AS MAIN_QRY WHERE ((id > ?)) ORDER BY id ASC LIMIT 100";
+ String s = pagingQueryProvider.generateRemainingPagesQuery(pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Override
+ @Test
+ public void testGenerateJumpToItemQueryWithGroupBy() {
+ pagingQueryProvider.setGroupClause("dep");
+ String sql = "SELECT id FROM foo WHERE bar = 1 GROUP BY dep ORDER BY id ASC LIMIT 1 OFFSET 99";
+ String s = pagingQueryProvider.generateJumpToItemQuery(145, pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Override
+ @Test
+ public void testGenerateJumpToItemQueryForFirstPageWithGroupBy() {
+ pagingQueryProvider.setGroupClause("dep");
+ String sql = "SELECT id FROM foo WHERE bar = 1 GROUP BY dep ORDER BY id ASC LIMIT 1 OFFSET 0";
+ String s = pagingQueryProvider.generateJumpToItemQuery(45, pageSize);
+ assertEquals(sql, s);
+ }
+
+ @Test
+ public void testFirstPageSqlWithAliases() {
+ Map sorts = new HashMap<>();
+ sorts.put("owner.id", Order.ASCENDING);
+
+ this.pagingQueryProvider = new HanaPagingQueryProvider();
+ this.pagingQueryProvider.setSelectClause("SELECT owner.id as ownerid, first_name, last_name, dog_name ");
+ this.pagingQueryProvider.setFromClause("FROM dog_owner owner INNER JOIN dog ON owner.id = dog.id ");
+ this.pagingQueryProvider.setSortKeys(sorts);
+
+ String firstPage = this.pagingQueryProvider.generateFirstPageQuery(5);
+ String jumpToItemQuery = this.pagingQueryProvider.generateJumpToItemQuery(7, 5);
+ String remainingPagesQuery = this.pagingQueryProvider.generateRemainingPagesQuery(5);
+
+ assertEquals("SELECT owner.id as ownerid, first_name, last_name, dog_name FROM dog_owner owner INNER JOIN dog ON owner.id = dog.id ORDER BY owner.id ASC LIMIT 5", firstPage);
+ assertEquals("SELECT owner.id FROM dog_owner owner INNER JOIN dog ON owner.id = dog.id ORDER BY owner.id ASC LIMIT 1 OFFSET 4", jumpToItemQuery);
+ assertEquals("SELECT owner.id as ownerid, first_name, last_name, dog_name FROM dog_owner owner INNER JOIN dog ON owner.id = dog.id WHERE ((owner.id > ?)) ORDER BY owner.id ASC LIMIT 5", remainingPagesQuery);
+ }
+
+ @Override
+ public String getFirstPageSqlWithMultipleSortKeys() {
+ return "SELECT id, name, age FROM foo WHERE bar = 1 ORDER BY name ASC, id DESC LIMIT 100";
+ }
+
+ @Override
+ public String getRemainingSqlWithMultipleSortKeys() {
+ return "SELECT id, name, age FROM foo WHERE (bar = 1) AND ((name > ?) OR (name = ? AND id < ?)) ORDER BY name ASC, id DESC LIMIT 100";
+ }
+
+ @Override
+ public String getJumpToItemQueryWithMultipleSortKeys() {
+ return "SELECT name, id FROM foo WHERE bar = 1 ORDER BY name ASC, id DESC LIMIT 1 OFFSET 99";
+ }
+
+ @Override
+ public String getJumpToItemQueryForFirstPageWithMultipleSortKeys() {
+ return "SELECT name, id FROM foo WHERE bar = 1 ORDER BY name ASC, id DESC LIMIT 1 OFFSET 0";
+ }
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java
index 2184260851..c34f069c99 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/support/DatabaseTypeTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2014 the original author or authors.
+ * Copyright 2008-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
import static org.springframework.batch.support.DatabaseType.SQLITE;
import static org.springframework.batch.support.DatabaseType.SQLSERVER;
import static org.springframework.batch.support.DatabaseType.SYBASE;
+import static org.springframework.batch.support.DatabaseType.HANA;
import static org.springframework.batch.support.DatabaseType.fromProductName;
/**
@@ -57,6 +58,7 @@ public void testFromProductName() {
assertEquals(POSTGRES, fromProductName("PostgreSQL"));
assertEquals(SYBASE, fromProductName("Sybase"));
assertEquals(SQLITE, fromProductName("SQLite"));
+ assertEquals(HANA, fromProductName("HDB"));
}
@Test(expected = IllegalArgumentException.class)
@@ -142,6 +144,12 @@ public void testFromMetaDataForSybase() throws Exception {
DataSource ds = DatabaseTypeTestUtils.getMockDataSource("Adaptive Server Enterprise");
assertEquals(SYBASE, DatabaseType.fromMetaData(ds));
}
+
+ @Test
+ public void testFromMetaDataForHana() throws Exception {
+ DataSource ds = DatabaseTypeTestUtils.getMockDataSource("HDB");
+ assertEquals(HANA, DatabaseType.fromMetaData(ds));
+ }
@Test(expected=MetaDataAccessException.class)
public void testBadMetaData() throws Exception {
diff --git a/spring-batch-infrastructure/src/test/resources/batch-hana.properties b/spring-batch-infrastructure/src/test/resources/batch-hana.properties
new file mode 100644
index 0000000000..f1adc30492
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/resources/batch-hana.properties
@@ -0,0 +1,14 @@
+# Placeholders batch.*
+# for SAP HANA:
+batch.jdbc.driver=com.sap.db.jdbc.Driver
+batch.jdbc.url=jdbc:sap://localhost:39015/
+batch.jdbc.user=SPRING_TEST
+batch.jdbc.password=Spr1ng_test
+batch.jdbc.testWhileIdle=false
+batch.jdbc.validationQuery=
+batch.schema.script=classpath:org/springframework/batch/item/database/init-foo-schema-hana.sql
+batch.business.schema.script=classpath:/org/springframework/batch/jms/init.sql
+batch.data.source.init=true
+batch.database.incrementer.class=org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer
+batch.database.incrementer.parent=sequenceIncrementerParent
+batch.verify.cursor.position=true
diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/init-foo-schema-hana.sql b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/init-foo-schema-hana.sql
new file mode 100644
index 0000000000..3055172feb
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/init-foo-schema-hana.sql
@@ -0,0 +1,25 @@
+DROP TABLE T_FOOS;
+DROP TABLE T_WRITE_FOOS;
+
+CREATE TABLE T_FOOS (
+ ID BIGINT NOT NULL,
+ NAME VARCHAR(45),
+ CODE VARCHAR(10),
+ VALUE BIGINT
+);
+
+ALTER TABLE T_FOOS ADD PRIMARY KEY (ID);
+
+INSERT INTO t_foos (id, name, value) VALUES (1, 'bar2', 2);
+INSERT INTO t_foos (id, name, value) VALUES (2, 'bar4', 4);
+INSERT INTO t_foos (id, name, value) VALUES (3, 'bar1', 1);
+INSERT INTO t_foos (id, name, value) VALUES (4, 'bar5', 5);
+INSERT INTO t_foos (id, name, value) VALUES (5, 'bar3', 3);
+
+CREATE TABLE T_WRITE_FOOS (
+ ID BIGINT NOT NULL,
+ NAME VARCHAR(45),
+ VALUE BIGINT
+);
+
+ALTER TABLE T_WRITE_FOOS ADD PRIMARY KEY (ID);
diff --git a/spring-batch-samples/src/main/resources/batch-hana.properties b/spring-batch-samples/src/main/resources/batch-hana.properties
new file mode 100644
index 0000000000..371460c48f
--- /dev/null
+++ b/spring-batch-samples/src/main/resources/batch-hana.properties
@@ -0,0 +1,20 @@
+# Placeholders batch.*
+# for SAP HANA:
+batch.jdbc.driver=com.sap.db.jdbc.Driver
+batch.jdbc.url=jdbc:sap://localhost:39015/
+batch.jdbc.user=SPRING_TEST
+batch.jdbc.password=Spr1ng_test
+batch.jdbc.testWhileIdle=false
+batch.jdbc.validationQuery=
+batch.drop.script=classpath:/org/springframework/batch/core/schema-drop-hana.sql
+batch.schema.script=classpath:/org/springframework/batch/core/schema-hana.sql
+batch.business.schema.script=business-schema-hana.sql
+batch.database.incrementer.class=org.springframework.jdbc.support.incrementer.HanaSequenceMaxValueIncrementer
+batch.database.incrementer.parent=sequenceIncrementerParent
+batch.lob.handler.class=org.springframework.jdbc.support.lob.DefaultLobHandler
+batch.grid.size=2
+batch.jdbc.pool.size=6
+batch.verify.cursor.position=true
+batch.isolationlevel=ISOLATION_SERIALIZABLE
+batch.table.prefix=BATCH_
+
diff --git a/spring-batch-samples/src/main/resources/business-schema-hana.sql b/spring-batch-samples/src/main/resources/business-schema-hana.sql
new file mode 100644
index 0000000000..8de565e5b9
--- /dev/null
+++ b/spring-batch-samples/src/main/resources/business-schema-hana.sql
@@ -0,0 +1,94 @@
+-- Autogenerated: do not edit this file
+-- You might need to remove this section the first time you run against a clean database
+DROP SEQUENCE BATCH_STAGING_SEQ ;
+DROP SEQUENCE TRADE_SEQ ;
+DROP SEQUENCE CUSTOMER_SEQ ;
+DROP TABLE BATCH_STAGING ;
+DROP TABLE TRADE ;
+DROP TABLE CUSTOMER ;
+DROP TABLE PLAYERS ;
+DROP TABLE GAMES ;
+DROP TABLE PLAYER_SUMMARY ;
+DROP TABLE ERROR_LOG ;
+
+-- Autogenerated: do not edit this file
+
+CREATE SEQUENCE CUSTOMER_SEQ START WITH 5 MINVALUE 0;
+CREATE SEQUENCE BATCH_STAGING_SEQ START WITH 0 MINVALUE 0;
+CREATE SEQUENCE TRADE_SEQ START WITH 0 MINVALUE 0;
+
+CREATE TABLE BATCH_STAGING (
+ ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ JOB_ID BIGINT NOT NULL,
+ VALUE BLOB NOT NULL,
+ PROCESSED CHAR(1) NOT NULL
+) ;
+
+CREATE TABLE TRADE (
+ ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ VERSION BIGINT ,
+ ISIN VARCHAR(45) NOT NULL,
+ QUANTITY BIGINT ,
+ PRICE DECIMAL(8,2) ,
+ CUSTOMER VARCHAR(45)
+) ;
+
+CREATE TABLE CUSTOMER (
+ ID BIGINT NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
+ VERSION BIGINT ,
+ NAME VARCHAR(45) ,
+ CREDIT DECIMAL(10,2)
+) ;
+
+INSERT INTO CUSTOMER (ID, VERSION, NAME, CREDIT) VALUES (1, 0, 'customer1', 100000);
+INSERT INTO CUSTOMER (ID, VERSION, NAME, CREDIT) VALUES (2, 0, 'customer2', 100000);
+INSERT INTO CUSTOMER (ID, VERSION, NAME, CREDIT) VALUES (3, 0, 'customer3', 100000);
+INSERT INTO CUSTOMER (ID, VERSION, NAME, CREDIT) VALUES (4, 0, 'customer4', 100000);
+
+CREATE TABLE PLAYERS (
+ PLAYER_ID CHAR(8) NOT NULL PRIMARY KEY,
+ LAST_NAME VARCHAR(35) NOT NULL,
+ FIRST_NAME VARCHAR(25) NOT NULL,
+ POS VARCHAR(10) ,
+ YEAR_OF_BIRTH BIGINT NOT NULL,
+ YEAR_DRAFTED BIGINT NOT NULL
+) ;
+
+CREATE TABLE GAMES (
+ PLAYER_ID CHAR(8) NOT NULL,
+ YEAR_NO BIGINT NOT NULL,
+ TEAM CHAR(3) NOT NULL,
+ WEEK BIGINT NOT NULL,
+ OPPONENT CHAR(3) ,
+ COMPLETES BIGINT ,
+ ATTEMPTS BIGINT ,
+ PASSING_YARDS BIGINT ,
+ PASSING_TD BIGINT ,
+ INTERCEPTIONS BIGINT ,
+ RUSHES BIGINT ,
+ RUSH_YARDS BIGINT ,
+ RECEPTIONS BIGINT ,
+ RECEPTIONS_YARDS BIGINT ,
+ TOTAL_TD BIGINT
+) ;
+
+CREATE TABLE PLAYER_SUMMARY (
+ ID CHAR(8) NOT NULL,
+ YEAR_NO BIGINT NOT NULL,
+ COMPLETES BIGINT NOT NULL ,
+ ATTEMPTS BIGINT NOT NULL ,
+ PASSING_YARDS BIGINT NOT NULL ,
+ PASSING_TD BIGINT NOT NULL ,
+ INTERCEPTIONS BIGINT NOT NULL ,
+ RUSHES BIGINT NOT NULL ,
+ RUSH_YARDS BIGINT NOT NULL ,
+ RECEPTIONS BIGINT NOT NULL ,
+ RECEPTIONS_YARDS BIGINT NOT NULL ,
+ TOTAL_TD BIGINT NOT NULL
+) ;
+
+CREATE TABLE ERROR_LOG (
+ JOB_NAME CHAR(20) ,
+ STEP_NAME CHAR(20) ,
+ MESSAGE VARCHAR(300) NOT NULL
+) ;
diff --git a/spring-batch-samples/src/main/sql/hana.properties b/spring-batch-samples/src/main/sql/hana.properties
new file mode 100644
index 0000000000..61dc0ff4c1
--- /dev/null
+++ b/spring-batch-samples/src/main/sql/hana.properties
@@ -0,0 +1,12 @@
+platform=hana
+# SQL language oddities
+BIGINT = BIGINT
+IDENTITY =
+GENERATED = GENERATED BY DEFAULT AS IDENTITY
+DOUBLE = DOUBLE
+DECIMAL = DECIMAL
+BLOB = BLOB
+TIMESTAMP = TIMESTAMP
+VARCHAR = VARCHAR
+# for generating drop statements...
+SEQUENCE = SEQUENCE
diff --git a/spring-batch-samples/src/main/sql/hana.vpp b/spring-batch-samples/src/main/sql/hana.vpp
new file mode 100644
index 0000000000..cf8238a41d
--- /dev/null
+++ b/spring-batch-samples/src/main/sql/hana.vpp
@@ -0,0 +1,2 @@
+#macro (sequence $name $value)CREATE SEQUENCE ${name} START WITH ${value} MINVALUE 0;
+#end