Integrate SAP Hana as supported Spring Batch database

This commit adds SAP HANA as a supported Spring Batch database, enabling
developers to seamlessly move their existing Spring Batch projects to SAP HANA
or easily starting new Spring Batch projects on SAP HANA.

This commit contains the following changes:

- Add SAP HANA to the DatabaseType enum
- Add HanaPagingQueryProvider and tests
- Add properties files for SAP HANA

Issue #2515
This commit is contained in:
Jonathan Bregler
2018-04-12 13:40:20 +02:00
committed by Mahmoud Ben Hassine
parent 44dd4f7f56
commit 9388abb589
23 changed files with 761 additions and 3 deletions

View File

@@ -92,6 +92,7 @@
<h2.version>2.1.210</h2.version>
<sqlite.version>3.36.0.3</sqlite.version>
<derby.version>10.14.2.0</derby.version>
<hana.version>2.9.12</hana.version>
<artemis.version>2.20.0</artemis.version>
<jaxb-core.version>3.0.2</jaxb-core.version>
<log4j.version>2.17.2</log4j.version>

View File

@@ -172,6 +172,12 @@
<version>${derby.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.db.jdbc</groupId>
<artifactId>ngdbc</artifactId>
<version>${hana.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

View File

@@ -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_

View File

@@ -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 ;

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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<SELF extends HANAContainer<SELF>> extends JdbcDatabaseContainer<SELF> {
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<String, String> sysctls = new HashMap<String, String>();
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<Integer> 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 ) + "/";
}
}

View File

@@ -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();
}
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.batch.item.database.support.AbstractSqlPagingQueryPro
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 @@ public class JdbcPagingItemReaderBuilder<T> {
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;

View File

@@ -25,6 +25,7 @@ import org.springframework.jdbc.support.incrementer.Db2MainframeMaxValueIncremen
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.DB2AS400;
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;
@@ -99,6 +101,9 @@ public class DefaultDataFieldMaxValueIncrementerFactory implements DataFieldMaxV
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);

View File

@@ -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);
}
}

View File

@@ -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.DB2ZOS;
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<PagingQuer
providers.put(DERBY,new DerbyPagingQueryProvider());
providers.put(HSQL,new HsqlPagingQueryProvider());
providers.put(H2,new H2PagingQueryProvider());
providers.put(HANA,new HanaPagingQueryProvider());
providers.put(MYSQL,new MySqlPagingQueryProvider());
providers.put(ORACLE,new OraclePagingQueryProvider());
providers.put(POSTGRES,new PostgresPagingQueryProvider());

View File

@@ -48,7 +48,8 @@ public enum DatabaseType {
POSTGRES("PostgreSQL"),
SYBASE("Sybase"),
H2("H2"),
SQLITE("SQLite");
SQLITE("SQLite"),
HANA("HDB");
private static final Map<String, DatabaseType> nameMap;

View File

@@ -24,6 +24,7 @@ import static org.mockito.Mockito.mock;
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;
@@ -63,6 +64,7 @@ public class DefaultDataFieldMaxValueIncrementerFactoryTests extends TestCase {
assertTrue(factory.isSupportedIncrementerType("sqlserver"));
assertTrue(factory.isSupportedIncrementerType("sybase"));
assertTrue(factory.isSupportedIncrementerType("sqlite"));
assertTrue(factory.isSupportedIncrementerType("hana"));
}
public void testUnsupportedDatabaseType(){
@@ -129,5 +131,9 @@ public class DefaultDataFieldMaxValueIncrementerFactoryTests extends TestCase {
public void testSqlite(){
assertTrue(factory.getIncrementer("sqlite", "NAME") instanceof SqliteMaxValueIncrementer);
}
public void testHana(){
assertTrue(factory.getIncrementer("hana", "NAME") instanceof HanaSequenceMaxValueIncrementer);
}
}

View File

@@ -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<String, Order> 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";
}
}

View File

@@ -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.POSTGRES;
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 class DatabaseTypeTests {
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 class DatabaseTypeTests {
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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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_

View File

@@ -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
) ;

View File

@@ -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

View File

@@ -0,0 +1,2 @@
#macro (sequence $name $value)CREATE SEQUENCE ${name} START WITH ${value} MINVALUE 0;
#end