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