├── .gitattributes ├── .github ├── codesigning.asc.enc ├── maven-settings.xml └── workflows │ └── database.yaml ├── .gitignore ├── LICENSE ├── README.md ├── demo ├── pom.xml └── src │ └── main │ └── java │ └── SqlInjection.java ├── findbugs-exclude.xml ├── log4j.xml ├── oracledb.sql ├── pom.xml ├── sample.properties ├── sqlserver.sql ├── src ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── susom │ │ │ └── database │ │ │ ├── Config.java │ │ │ ├── ConfigFrom.java │ │ │ ├── ConfigFromImpl.java │ │ │ ├── ConfigImpl.java │ │ │ ├── ConfigInvalidException.java │ │ │ ├── ConfigMissingException.java │ │ │ ├── ConstraintViolationException.java │ │ │ ├── Database.java │ │ │ ├── DatabaseException.java │ │ │ ├── DatabaseImpl.java │ │ │ ├── DatabaseMock.java │ │ │ ├── DatabaseProvider.java │ │ │ ├── DatabaseProviderVertx.java │ │ │ ├── DbCode.java │ │ │ ├── DbCodeTx.java │ │ │ ├── DbCodeTyped.java │ │ │ ├── DbCodeTypedTx.java │ │ │ ├── Ddl.java │ │ │ ├── DdlImpl.java │ │ │ ├── DebugSql.java │ │ │ ├── Flavor.java │ │ │ ├── InternalStringReader.java │ │ │ ├── Metric.java │ │ │ ├── MixedParameterSql.java │ │ │ ├── Options.java │ │ │ ├── OptionsDefault.java │ │ │ ├── OptionsOverride.java │ │ │ ├── QueryTimedOutException.java │ │ │ ├── Row.java │ │ │ ├── RowHandler.java │ │ │ ├── RowStub.java │ │ │ ├── Rows.java │ │ │ ├── RowsAdaptor.java │ │ │ ├── RowsHandler.java │ │ │ ├── Schema.java │ │ │ ├── Sql.java │ │ │ ├── SqlArgs.java │ │ │ ├── SqlInsert.java │ │ │ ├── SqlInsertImpl.java │ │ │ ├── SqlSelect.java │ │ │ ├── SqlSelectImpl.java │ │ │ ├── SqlUpdate.java │ │ │ ├── SqlUpdateImpl.java │ │ │ ├── StatementAdaptor.java │ │ │ ├── Transaction.java │ │ │ ├── TransactionImpl.java │ │ │ ├── VertxUtil.java │ │ │ ├── When.java │ │ │ └── WrongNumberOfRowsException.java │ └── resources │ │ ├── Database.astub │ │ ├── Sql.astub │ │ ├── SqlInsert.astub │ │ └── When.astub └── test │ ├── java │ └── com │ │ └── github │ │ └── susom │ │ └── database │ │ ├── example │ │ ├── DerbyExample.java │ │ ├── DynamicSql.java │ │ ├── FakeBuilder.java │ │ ├── HelloAny.java │ │ ├── HelloDerby.java │ │ ├── InsertReturning.java │ │ ├── JettyServer.java │ │ ├── Sample.java │ │ ├── SampleDao.java │ │ ├── VertxServer.java │ │ └── VertxServerFastAndSlow.java │ │ └── test │ │ ├── CommonTest.java │ │ ├── ConfigTest.java │ │ ├── DatabaseTest.java │ │ ├── DemoTest.java │ │ ├── DerbyTest.java │ │ ├── HsqldbTest.java │ │ ├── OracleTest.java │ │ ├── PostgreSqlTest.java │ │ ├── Retry.java │ │ ├── Retryable.java │ │ ├── RowStubMockDao.java │ │ ├── RowStubMockData.java │ │ ├── RowStubTest.java │ │ ├── SampleDaoTest.java │ │ ├── SqlArgsTest.java │ │ ├── SqlServerTest.java │ │ ├── VertxLoggingTest.java │ │ └── VertxProviderTest.java │ └── resources │ └── log4j.xml ├── test-oracle.sh ├── test-postgres.sh ├── test-sqlserver.sh └── vagrant ├── postgresql-9.3 ├── Vagrantfile └── box-init.sh ├── postgresql-9.4 ├── Vagrantfile └── box-init.sh └── postgresql-9.5 ├── Vagrantfile └── box-init.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.java text diff=java 7 | *.html text diff=html 8 | *.css text 9 | *.js text 10 | *.sql text 11 | *.xml text 12 | *.md text 13 | *.txt text 14 | *.properties text 15 | NOTICE text 16 | LICENSE text 17 | 18 | # Denote all files that are truly binary and should not be modified. 19 | *.png binary 20 | *.jpg binary 21 | *.gif binary -------------------------------------------------------------------------------- /.github/codesigning.asc.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/susom/database/6c7462f29e69b1b7fc5e03f80d5e9a97853c2fb0/.github/codesigning.asc.enc -------------------------------------------------------------------------------- /.github/maven-settings.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | *,!artifact-registry 8 | https://repo.maven.apache.org/maven2/ 9 | 10 | 11 | 12 | 13 | 14 | ci-build 15 | 16 | 17 | artifact-registry 18 | https://us-west1-maven.pkg.dev/som-rit-infrastructure-prod/public-maven 19 | 20 | false 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | never 29 | 30 | 31 | false 32 | 33 | central 34 | Central Repository 35 | https://repo.maven.apache.org/maven2 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ci-build 44 | 45 | 46 | 47 | 48 | 49 | artifact-registry 50 | oauth2accesstoken 51 | ${env.ACCESS_TOKEN} 52 | 53 | 54 | 55 | ossrh 56 | ${env.OSSRH_USERNAME} 57 | ${env.OSSRH_PASSWORD} 58 | 59 | 60 | 61 | github.com 62 | ${env.GITHUB_USERNAME} 63 | ${env.GITHUB_TOKEN} 64 | 65 | 66 | -------------------------------------------------------------------------------- /.github/workflows/database.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * *" 9 | 10 | permissions: 11 | id-token: write 12 | contents: read 13 | 14 | jobs: 15 | build-and-test: 16 | name: Build and Test 17 | runs-on: ubuntu-latest 18 | 19 | services: 20 | docker: 21 | image: docker:20.10.7 22 | 23 | steps: 24 | - name: Checkout code 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 # Shallow clones should be disabled for a better SonarCloud analysis 28 | 29 | - name: Set up JDK 17 30 | uses: actions/setup-java@v4 31 | with: 32 | java-version: '17' 33 | distribution: 'temurin' 34 | 35 | - name: Cache SonarCloud packages 36 | uses: actions/cache@v4 37 | with: 38 | path: ~/.sonar/cache 39 | key: ${{ runner.os }}-sonar 40 | restore-keys: ${{ runner.os }}-sonar 41 | 42 | - name: Cache Maven dependencies 43 | uses: actions/cache@v4 44 | with: 45 | path: ~/.m2 46 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 47 | restore-keys: ${{ runner.os }}-maven 48 | 49 | - name: Set timezone 50 | run: | 51 | export TZ=America/Los_Angeles 52 | 53 | - name: Setup Maven settings 54 | run: | 55 | cp .github/maven-settings.xml $HOME/.m2/settings.xml 56 | sed -i "s/-SNAPSHOT/-github-build-${{ github.run_number }}/" pom.xml 57 | 58 | - name: Authenticate to Google Cloud 59 | uses: google-github-actions/auth@v2 60 | with: 61 | project_id: ${{ secrets.WORKLOAD_IDENTITY_PROJECT }} 62 | workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} 63 | create_credentials_file: true 64 | export_environment_variables: true 65 | cleanup_credentials: true 66 | 67 | - name: Set up OAuth2 access token for Docker 68 | run: | 69 | echo "ACCESS_TOKEN=$(gcloud auth print-access-token)" >> $GITHUB_ENV 70 | 71 | - name: Docker login to Artifact Registry 72 | run: echo "${{ env.ACCESS_TOKEN }}" | docker login -u oauth2accesstoken --password-stdin https://us-west1-docker.pkg.dev 73 | 74 | - name: Test and Build with Maven 75 | env: 76 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 77 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 78 | run: | 79 | mvn -e -Dfailsafe.rerunFailingTestsCount=3 -Dmaven.javadoc.skip=true \ 80 | "-Duser.timezone=America/Los_Angeles" \ 81 | "-Dhsqldb.database.url=jdbc:hsqldb:file:target/hsqldb;shutdown=true" \ 82 | -Dhsqldb.database.user=SA -Dhsqldb.database.password= -Pcoverage,hsqldb verify && 83 | bash test-postgres.sh && 84 | bash test-sqlserver.sh && 85 | bash test-oracle.sh && 86 | mvn -e org.jacoco:jacoco-maven-plugin:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=susom_database 87 | 88 | - name: Display Surefire reports on failure 89 | if: failure() 90 | run: | 91 | for F in target/surefire-reports/*.txt; do echo $F; cat $F; echo; done 92 | 93 | # deploys into Artifact Registry when code is pushed to the master branch of the 'susom/database' repository. 94 | deploy-snapshots: 95 | name: Deploy to Artifact Registry 96 | runs-on: ubuntu-latest 97 | if: github.repository == 'susom/database' && github.ref == 'refs/heads/master' && github.event_name == 'push' 98 | needs: build-and-test 99 | steps: 100 | - name: Checkout code 101 | uses: actions/checkout@v4 102 | 103 | - name: Set up JDK 17 104 | uses: actions/setup-java@v4 105 | with: 106 | java-version: '17' 107 | distribution: 'temurin' 108 | 109 | - name: Cache Maven dependencies 110 | uses: actions/cache@v4 111 | with: 112 | path: ~/.m2 113 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 114 | restore-keys: ${{ runner.os }}-maven 115 | 116 | - name: Setup Maven settings 117 | run: | 118 | cp .github/maven-settings.xml $HOME/.m2/settings.xml 119 | sed -i "s/-SNAPSHOT/-github-build-${{ github.run_number }}/" pom.xml 120 | 121 | - name: Authenticate to Google Cloud 122 | uses: google-github-actions/auth@v2 123 | with: 124 | project_id: ${{ secrets.WORKLOAD_IDENTITY_PROJECT }} 125 | workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} 126 | create_credentials_file: true 127 | export_environment_variables: true 128 | cleanup_credentials: true 129 | 130 | - name: Set up OAuth2 access token for Maven 131 | run: | 132 | echo "ACCESS_TOKEN=$(gcloud auth print-access-token)" >> $GITHUB_ENV 133 | 134 | - name: Deploy to Maven 135 | run: mvn --batch-mode -e -DskipTests=true deploy 136 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | build 3 | out 4 | classes 5 | .idea 6 | *.iml 7 | local.properties 8 | .vagrant 9 | .DS_Store 10 | dependency-reduced-pom.xml 11 | -------------------------------------------------------------------------------- /demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | database-demo 8 | database-demo 9 | 5.0-SNAPSHOT 10 | 11 | 12 | 3.40.0 13 | UTF-8 14 | 15 | 16 | 17 | 18 | com.github.susom 19 | database 20 | 5.0-SNAPSHOT 21 | 22 | 23 | 24 | org.checkerframework 25 | checker-qual 26 | ${checkerframework.version} 27 | provided 28 | 29 | 30 | 31 | 32 | 33 | 34 | maven-dependency-plugin 35 | 36 | 37 | 38 | properties 39 | 40 | 41 | 42 | 43 | 44 | org.apache.maven.plugins 45 | maven-compiler-plugin 46 | 3.11.0 47 | 48 | 17 49 | 17 50 | true 51 | 52 | 53 | org.checkerframework 54 | checker 55 | ${checkerframework.version} 56 | 57 | 58 | 59 | org.checkerframework.checker.tainting.TaintingChecker 60 | 61 | 62 | -J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED 63 | -J--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED 64 | -J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED 65 | -J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED 66 | -J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED 67 | -J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED 68 | -J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED 69 | -J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED 70 | -J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED 71 | -Astubs=${com.github.susom:database:jar} 72 | 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-surefire-plugin 78 | 2.20 79 | 80 | true 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /demo/src/main/java/SqlInjection.java: -------------------------------------------------------------------------------- 1 | import com.github.susom.database.Database; 2 | import com.github.susom.database.Flavor; 3 | import com.github.susom.database.Sql; 4 | 5 | /** 6 | * Demo of using the checker framework to detect SQL injections. To see how 7 | * this works, download The Checker Framework (http://types.cs.washington.edu/checker-framework/), 8 | * adjust the checker.dir property in pom.xml, and execute this Maven command: 9 | * 10 | *

mvn -Pchecker verify

11 | */ 12 | public class SqlInjection { 13 | void example(Database db, String[] args) { 14 | String tainted = args[0]; 15 | 16 | // Checker will flag each of these as a type error 17 | System.out.println(db.toSelect(tainted).queryLongOrNull()); 18 | db.toInsert(tainted).insert(1); 19 | db.toUpdate(tainted).update(1); 20 | db.toDelete(tainted).update(1); 21 | 22 | Sql sql = new Sql(tainted); 23 | sql.append(tainted); 24 | 25 | // These two lines are actually ok, since the Sql class should be untainted 26 | db.toInsert(sql.sql()).insert(1); 27 | System.out.println(db.toSelect(sql).queryLongOrNull()); 28 | 29 | // These are ok (no use of tainted input) 30 | db.toInsert(someSql().sql()).insert(1); 31 | db.toInsert("" + db.when()).insert(1); 32 | db.toInsert("" + db.when().derby("a")).insert(1); 33 | db.toInsert(db.when().derby("a").other("b")).insert(1); 34 | 35 | // Should be ok, but flagged right now because checker doesn't understand enum stubs 36 | db.toInsert("" + Flavor.postgresql.typeStringVar(1)); 37 | 38 | // But this is another illegal use 39 | db.toInsert(db.when().derby(tainted).other("")).insert(1); 40 | } 41 | 42 | Sql someSql() { 43 | return new Sql().append("bar" + "1").argString("baz"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /oracledb.sql: -------------------------------------------------------------------------------- 1 | ALTER SESSION SET "_ORACLE_SCRIPT"=true; 2 | CREATE USER testuser IDENTIFIED BY "TestPassword456"; 3 | GRANT CREATE SESSION TO testuser; 4 | GRANT UNLIMITED TABLESPACE TO testuser; 5 | GRANT DBA TO testuser; 6 | QUIT; 7 | -------------------------------------------------------------------------------- /sample.properties: -------------------------------------------------------------------------------- 1 | ### Do not modify this file for your local development. 2 | ### Copy this file to one named local.properties using the following command: 3 | ### 4 | ### grep -v '###' sample.properties | sed 's/SECRET_PASSWORD/'`openssl rand -base64 18 | tr -d +/`'/g' > local.properties 5 | ### 6 | ### For security, the above copy command will also generate secure passwords. 7 | 8 | database.url=jdbc:oracle:thin:@localhost:1521:ORCL 9 | database.user=scott 10 | database.password=tiger 11 | 12 | # To create a local database run this command: 13 | # docker run -d --name dbtest-pg -e POSTGRES_PASSWORD=SECRET_PASSWORD -p 5432:5432/tcp postgres:14 14 | postgres.database.url=jdbc:postgresql://localhost/postgres 15 | postgres.database.user=postgres 16 | postgres.database.password=SECRET_PASSWORD 17 | 18 | sqlserver.database.url=jdbc:sqlserver://localhost:1433;databaseName=test 19 | sqlserver.database.user=test 20 | sqlserver.database.password=test 21 | 22 | hsqldb.database.url=jdbc:hsqldb:file:target/hsqldb;shutdown=true 23 | hsqldb.database.user=SA 24 | hsqldb.database.password= 25 | -------------------------------------------------------------------------------- /sqlserver.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE testDB 2 | GO 3 | CREATE LOGIN test WITH PASSWORD = 'TestPwd@345' 4 | GO 5 | USE testDB 6 | GO 7 | CREATE USER test FOR LOGIN test 8 | GO 9 | EXEC sp_addrolemember 'db_owner', test 10 | GO 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/Config.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.function.Function; 5 | import java.util.function.Supplier; 6 | 7 | import javax.annotation.Nonnull; 8 | import javax.annotation.Nullable; 9 | 10 | /** 11 | * Entry point for getting configuration parameters. This isn't intended as 12 | * a be-all, end-all configuration solution. Just a way of easily specifying 13 | * multiple read-only sources for configuration with a nice fluent syntax. 14 | * 15 | * @author garricko 16 | */ 17 | public interface Config extends Function, Supplier { 18 | /** 19 | * Convenience method for fluent syntax. 20 | * 21 | * @return a builder for specifying from where configuration should be loaded 22 | */ 23 | static @Nonnull ConfigFrom from() { 24 | return new ConfigFromImpl(); 25 | } 26 | 27 | // TODO add: String originalKey(String key) to find out the key before prefixing or other manipulation 28 | 29 | /** 30 | * @return a trimmed, non-empty string, or null 31 | */ 32 | @Nullable String getString(@Nonnull String key); 33 | 34 | /** 35 | * @return a trimmed, non-empty string 36 | * @throws ConfigMissingException if no value could be read for the specified key 37 | */ 38 | @Nonnull String getStringOrThrow(@Nonnull String key); 39 | 40 | @Nonnull String getString(String key, @Nonnull String defaultValue); 41 | 42 | /** 43 | * Same as {@link #getString(String)}. Useful for passing configs around 44 | * without static dependencies. 45 | */ 46 | @Override 47 | default String apply(String key) { 48 | return getString(key); 49 | } 50 | 51 | @Override 52 | default Config get() { return this; } 53 | 54 | @Nullable Integer getInteger(@Nonnull String key); 55 | 56 | int getInteger(@Nonnull String key, int defaultValue); 57 | 58 | /** 59 | * @throws ConfigMissingException if no value could be read for the specified key 60 | */ 61 | int getIntegerOrThrow(@Nonnull String key); 62 | 63 | @Nullable Long getLong(@Nonnull String key); 64 | 65 | long getLong(@Nonnull String key, long defaultValue); 66 | 67 | /** 68 | * @throws ConfigMissingException if no value could be read for the specified key 69 | */ 70 | long getLongOrThrow(@Nonnull String key); 71 | 72 | @Nullable Float getFloat(@Nonnull String key); 73 | 74 | float getFloat(@Nonnull String key, float defaultValue); 75 | 76 | /** 77 | * @throws ConfigMissingException if no value could be read for the specified key 78 | */ 79 | float getFloatOrThrow(@Nonnull String key); 80 | 81 | @Nullable Double getDouble(@Nonnull String key); 82 | 83 | double getDouble(@Nonnull String key, double defaultValue); 84 | 85 | /** 86 | * @throws ConfigMissingException if no value could be read for the specified key 87 | */ 88 | double getDoubleOrThrow(@Nonnull String key); 89 | 90 | @Nullable 91 | BigDecimal getBigDecimal(@Nonnull String key); 92 | 93 | @Nonnull BigDecimal getBigDecimal(String key, @Nonnull BigDecimal defaultValue); 94 | 95 | /** 96 | * @throws ConfigMissingException if no value could be read for the specified key 97 | */ 98 | @Nonnull BigDecimal getBigDecimalOrThrow(String key); 99 | 100 | /** 101 | * Read a boolean value from the configuration. The value is not case-sensitivie, 102 | * and may be either true/false or yes/no. If no value was provided or an invalid 103 | * value is provided, false will be returned. 104 | */ 105 | boolean getBooleanOrFalse(@Nonnull String key); 106 | 107 | /** 108 | * Read a boolean value from the configuration. The value is not case-sensitivie, 109 | * and may be either true/false or yes/no. If no value was provided or an invalid 110 | * value is provided, true will be returned. 111 | */ 112 | boolean getBooleanOrTrue(@Nonnull String key); 113 | 114 | /** 115 | * @throws ConfigMissingException if no value could be read for the specified key 116 | */ 117 | boolean getBooleanOrThrow(@Nonnull String key); 118 | 119 | /** 120 | * Show where configuration is coming from. This is useful to drop in your logs 121 | * for troubleshooting. 122 | */ 123 | String sources(); 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/ConfigFrom.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | import java.io.File; 4 | import java.nio.charset.CharsetDecoder; 5 | import java.util.Properties; 6 | import java.util.function.Function; 7 | import java.util.function.Supplier; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | /** 12 | * Pull configuration properties from various sources and filter/manipulate them. 13 | * 14 | * @author garricko 15 | */ 16 | public interface ConfigFrom extends Supplier { 17 | /** 18 | * Convenience method for fluent syntax. 19 | * 20 | * @return a builder for specifying from where configuration should be loaded 21 | */ 22 | @Nonnull 23 | static ConfigFrom firstOf() { 24 | return new ConfigFromImpl(); 25 | } 26 | 27 | @Nonnull 28 | static Config other(Function other) { 29 | if (other instanceof Config) { 30 | return (Config) other; 31 | } 32 | return new ConfigFromImpl().custom(other::apply).get(); 33 | } 34 | 35 | ConfigFrom custom(Function keyValueLookup); 36 | 37 | ConfigFrom value(String key, String value); 38 | 39 | ConfigFrom systemProperties(); 40 | 41 | ConfigFrom env(); 42 | 43 | ConfigFrom properties(Properties properties); 44 | 45 | ConfigFrom config(Config config); 46 | 47 | ConfigFrom config(Supplier config); 48 | 49 | /** 50 | * Adds a set of properties files to read from, which can be overridden by a system property "properties". 51 | * Equivalent to: 52 | *
 53 |    *   defaultPropertyFiles("properties", "conf/app.properties", "local.properties", "sample.properties")
 54 |    * 
55 | */ 56 | ConfigFrom defaultPropertyFiles(); 57 | 58 | /** 59 | * Adds a set of properties files to read from, which can be overridden by a specified system property. 60 | * Equivalent to: 61 | *
 62 |    *   defaultPropertyFiles(systemPropertyKey, Charset.defaultCharset().newDecoder(), filenames)
 63 |    * 
64 | */ 65 | ConfigFrom defaultPropertyFiles(String systemPropertyKey, String... filenames); 66 | 67 | /** 68 | * Adds a set of properties files to read from, which can be overridden by a specified system property. 69 | * Equivalent to: 70 | *
 71 |    *   propertyFile(Charset.defaultCharset().newDecoder(),
 72 |    *       System.getProperty(systemPropertyKey, String.join(File.pathSeparator, filenames))
 73 |    *       .split(File.pathSeparator));
 74 |    * 
75 | */ 76 | ConfigFrom defaultPropertyFiles(String systemPropertyKey, CharsetDecoder decoder, String... filenames); 77 | 78 | ConfigFrom propertyFile(String... filenames); 79 | 80 | ConfigFrom propertyFile(CharsetDecoder decoder, String... filenames); 81 | 82 | ConfigFrom propertyFile(File... files); 83 | 84 | ConfigFrom propertyFile(CharsetDecoder decoder, File... files); 85 | 86 | ConfigFrom rename(String key, String newKey); 87 | 88 | ConfigFrom includeKeys(String... keys); 89 | 90 | ConfigFrom includePrefix(String... prefixes); 91 | 92 | ConfigFrom includeRegex(String regex); 93 | 94 | ConfigFrom excludeKeys(String... keys); 95 | 96 | ConfigFrom excludePrefix(String... prefixes); 97 | 98 | ConfigFrom excludeRegex(String regex); 99 | 100 | ConfigFrom removePrefix(String... prefixes); 101 | 102 | ConfigFrom addPrefix(String prefix); 103 | 104 | ConfigFrom substitutions(Config config); 105 | 106 | Config get(); 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/ConfigImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | import java.util.function.Function; 7 | 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | /** 15 | * This class handles all of the type conversions, default values, etc. 16 | * to get from simple string key/value pairs to richer configuration. 17 | * 18 | * @author garricko 19 | */ 20 | public class ConfigImpl implements Config { 21 | private static final Logger log = LoggerFactory.getLogger(ConfigFromImpl.class); 22 | private final Function provider; 23 | private final String sources; 24 | private Set failedKeys = new HashSet<>(); 25 | 26 | public ConfigImpl(Function provider, String sources) { 27 | this.provider = provider; 28 | this.sources = sources; 29 | } 30 | 31 | @Override 32 | public String getString(@Nonnull String key) { 33 | return cleanString(key); 34 | } 35 | 36 | @Nonnull 37 | @Override 38 | public String getStringOrThrow(@Nonnull String key) { 39 | return nonnull(key, getString(key)); 40 | } 41 | 42 | @Nonnull 43 | @Override 44 | public String getString(String key, @Nonnull String defaultValue) { 45 | String stringValue = cleanString(key); 46 | if (stringValue != null) { 47 | return stringValue; 48 | } 49 | // Make sure the default value is tidied the same way a value would be 50 | defaultValue = defaultValue.trim(); 51 | if (defaultValue.length() == 0) { 52 | throw new IllegalArgumentException("Your default value is empty or just whitespace"); 53 | } 54 | return defaultValue; 55 | } 56 | 57 | @Override 58 | public Integer getInteger(@Nonnull String key) { 59 | String stringValue = cleanString(key); 60 | try { 61 | return stringValue == null ? null : Integer.parseInt(stringValue); 62 | } catch (Exception e) { 63 | if (!failedKeys.contains(key)) { 64 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 65 | failedKeys.add(key); 66 | } 67 | return null; 68 | } 69 | } 70 | 71 | @Override 72 | public int getInteger(@Nonnull String key, int defaultValue) { 73 | Integer value = getInteger(key); 74 | return value == null ? defaultValue : value; 75 | } 76 | 77 | @Override 78 | public int getIntegerOrThrow(@Nonnull String key) { 79 | return nonnull(key, getInteger(key)); 80 | } 81 | 82 | @Nullable 83 | @Override 84 | public Long getLong(@Nonnull String key) { 85 | String stringValue = cleanString(key); 86 | try { 87 | return stringValue == null ? null : Long.parseLong(stringValue); 88 | } catch (Exception e) { 89 | if (!failedKeys.contains(key)) { 90 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 91 | failedKeys.add(key); 92 | } 93 | return null; 94 | } 95 | } 96 | 97 | @Override 98 | public long getLong(@Nonnull String key, long defaultValue) { 99 | Long value = getLong(key); 100 | return value == null ? defaultValue : value; 101 | } 102 | 103 | @Override 104 | public long getLongOrThrow(@Nonnull String key) { 105 | return nonnull(key, getLong(key)); 106 | } 107 | 108 | @Nullable 109 | @Override 110 | public Float getFloat(@Nonnull String key) { 111 | String stringValue = cleanString(key); 112 | try { 113 | return stringValue == null ? null : Float.parseFloat(stringValue); 114 | } catch (Exception e) { 115 | if (!failedKeys.contains(key)) { 116 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 117 | failedKeys.add(key); 118 | } 119 | return null; 120 | } 121 | } 122 | 123 | @Override 124 | public float getFloat(@Nonnull String key, float defaultValue) { 125 | Float value = getFloat(key); 126 | return value == null ? defaultValue : value; 127 | } 128 | 129 | @Override 130 | public float getFloatOrThrow(@Nonnull String key) { 131 | return nonnull(key, getFloat(key)); 132 | } 133 | 134 | @Nullable 135 | @Override 136 | public Double getDouble(@Nonnull String key) { 137 | String stringValue = cleanString(key); 138 | try { 139 | return stringValue == null ? null : Double.parseDouble(stringValue); 140 | } catch (Exception e) { 141 | if (!failedKeys.contains(key)) { 142 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 143 | failedKeys.add(key); 144 | } 145 | return null; 146 | } 147 | } 148 | 149 | @Override 150 | public double getDouble(@Nonnull String key, double defaultValue) { 151 | Double value = getDouble(key); 152 | return value == null ? defaultValue : value; 153 | } 154 | 155 | @Override 156 | public double getDoubleOrThrow(@Nonnull String key) { 157 | return nonnull(key, getDouble(key)); 158 | } 159 | 160 | @Nullable 161 | @Override 162 | public BigDecimal getBigDecimal(@Nonnull String key) { 163 | String stringValue = cleanString(key); 164 | try { 165 | return stringValue == null ? null : new BigDecimal(stringValue); 166 | } catch (Exception e) { 167 | if (!failedKeys.contains(key)) { 168 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 169 | failedKeys.add(key); 170 | } 171 | return null; 172 | } 173 | } 174 | 175 | @Nonnull 176 | @Override 177 | public BigDecimal getBigDecimal(String key, @Nonnull BigDecimal defaultValue) { 178 | BigDecimal value = getBigDecimal(key); 179 | return value == null ? defaultValue : value; 180 | } 181 | 182 | @Nonnull 183 | @Override 184 | public BigDecimal getBigDecimalOrThrow(String key) { 185 | return nonnull(key, getBigDecimal(key)); 186 | } 187 | 188 | @Override 189 | public boolean getBooleanOrFalse(@Nonnull String key) { 190 | return parseBoolean(cleanString(key), false); 191 | } 192 | 193 | @Override 194 | public boolean getBooleanOrTrue(@Nonnull String key) { 195 | return parseBoolean(cleanString(key), true); 196 | } 197 | 198 | @Override 199 | public boolean getBooleanOrThrow(@Nonnull String key) { 200 | String value = nonnull(key, cleanString(key)); 201 | value = value.toLowerCase(); 202 | if (value.equals("yes") || value.equals("true")) { 203 | return true; 204 | } 205 | if (value.equals("no") || value.equals("false")) { 206 | return false; 207 | } 208 | throw new ConfigMissingException("Unrecognized boolean value for config key: " + key); 209 | } 210 | 211 | @Override 212 | public String sources() { 213 | return sources; 214 | } 215 | 216 | private T nonnull(String key, T value) { 217 | if (value == null) { 218 | throw new ConfigMissingException("No value for config key: " + key); 219 | } 220 | return value; 221 | } 222 | 223 | private boolean parseBoolean(String value, boolean defaultValue) { 224 | if (value != null) { 225 | value = value.toLowerCase(); 226 | if (value.equals("yes") || value.equals("true")) { 227 | return true; 228 | } 229 | if (value.equals("no") || value.equals("false")) { 230 | return false; 231 | } 232 | } 233 | return defaultValue; 234 | } 235 | 236 | private String cleanString(String key) { 237 | String value = null; 238 | try { 239 | value = provider.apply(key); 240 | if (value != null) { 241 | value = value.trim(); 242 | if (value.length() == 0) { 243 | value = null; 244 | } 245 | } 246 | } catch (Exception e) { 247 | if (!failedKeys.contains(key)) { 248 | log.warn("Could not load config value for key (this message will only be logged once): " + key, e); 249 | failedKeys.add(key); 250 | } 251 | } 252 | 253 | return value; 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/ConfigInvalidException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | /** 19 | * Indicates that a configuration value is present but not in a usable format. 20 | * For example, if the value must be an integer and the configuration value 21 | * is a non-numeric string. 22 | * 23 | * @author garricko 24 | */ 25 | public class ConfigInvalidException extends DatabaseException { 26 | public ConfigInvalidException(String message) { 27 | super(message); 28 | } 29 | 30 | public ConfigInvalidException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | public ConfigInvalidException(String message, Throwable cause) { 35 | super(message, cause); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/ConfigMissingException.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | /** 4 | * Indicates that a configuration value is required but was not present. 5 | * 6 | * @author garricko 7 | */ 8 | public class ConfigMissingException extends DatabaseException { 9 | public ConfigMissingException(String message) { 10 | super(message); 11 | } 12 | 13 | public ConfigMissingException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public ConfigMissingException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/ConstraintViolationException.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | /** 4 | * This exception will be thrown when a condition arises that violates 5 | * a stated invariant regarding the database. This might be a database 6 | * schema "constraint violated" as thrown by the database, or could be 7 | * caused by a violation of constraints enforced only within the code. 8 | */ 9 | public class ConstraintViolationException extends DatabaseException { 10 | public ConstraintViolationException(String message) { 11 | super(message); 12 | } 13 | 14 | public ConstraintViolationException(Throwable cause) { 15 | super(cause); 16 | } 17 | 18 | public ConstraintViolationException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DatabaseException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Indicates something went wrong accessing the database. Most often this is 21 | * used to wrap SQLException to avoid declaring checked exceptions. 22 | * 23 | * @author garricko 24 | */ 25 | public class DatabaseException extends RuntimeException { 26 | public DatabaseException(String message) { 27 | super(message); 28 | } 29 | 30 | public DatabaseException(Throwable cause) { 31 | super(cause); 32 | } 33 | 34 | public DatabaseException(String message, Throwable cause) { 35 | super(message, cause); 36 | } 37 | 38 | /** 39 | * Wrap an exception with a DatabaseException, taking into account all known 40 | * subtypes such that we wrap subtypes in a matching type (so we don't obscure 41 | * the type available to catch clauses). 42 | * 43 | * @param message the new wrapping exception will have this message 44 | * @param cause the exception to be wrapped 45 | * @return the exception you should throw 46 | */ 47 | public static DatabaseException wrap(String message, Throwable cause) { 48 | if (cause instanceof ConstraintViolationException) { 49 | return new ConstraintViolationException(message, cause); 50 | } 51 | return new DatabaseException(message, cause); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DatabaseMock.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | /** 4 | * Convenience class to intercept calls to Connection and return stubbed results 5 | * for testing purposes. 6 | */ 7 | public interface DatabaseMock { 8 | RowStub query(String executeSql, String debugSql); 9 | 10 | Integer insert(String executeSql, String debugSql); 11 | 12 | Long insertReturningPk(String executeSql, String debugSql); 13 | 14 | RowStub insertReturning(String executeSql, String debugSql); 15 | 16 | Integer update(String executeSql, String debugSql); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DbCode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | import java.util.function.Supplier; 19 | 20 | /** 21 | * A block of runnable code using a transacted Database. 22 | * 23 | * @author garricko 24 | */ 25 | public interface DbCode { 26 | /** 27 | * Implement this method to provide a block of code that uses the provided database 28 | * and is transacted. Whether the transaction will commit or rollback is typically 29 | * controlled by the code that invokes this method. 30 | * 31 | *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in 32 | * a DatabaseException (if it is not already one), and then propagated.

33 | */ 34 | void run(Supplier dbs) throws Exception; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DbCodeTx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | import java.util.function.Supplier; 19 | 20 | /** 21 | * A block of runnable code using a transacted Database. 22 | * 23 | * @author garricko 24 | */ 25 | public interface DbCodeTx { 26 | /** 27 | * Implement this method to provide a block of code that uses the provided database 28 | * and is transacted. Whether the transaction will commit or rollback is typically 29 | * controlled by the code that invokes this method. 30 | * 31 | *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in 32 | * a DatabaseException (if it is not already one), and then propagated.

33 | */ 34 | void run(Supplier db, Transaction tx) throws Exception; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DbCodeTyped.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | import java.util.function.Supplier; 19 | 20 | /** 21 | * A block of runnable code using a transacted Database. 22 | * 23 | * @author garricko 24 | */ 25 | public interface DbCodeTyped { 26 | /** 27 | * Implement this method to provide a block of code that uses the provided database 28 | * and is transacted. Whether the transaction will commit or rollback is typically 29 | * controlled by the code that invokes this method. 30 | * 31 | *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in 32 | * a DatabaseException (if it is not already one), and then propagated.

33 | */ 34 | T run(Supplier dbs) throws Exception; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DbCodeTypedTx.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | import java.util.function.Supplier; 19 | 20 | /** 21 | * A block of runnable code using a transacted Database. 22 | * 23 | * @author garricko 24 | */ 25 | public interface DbCodeTypedTx { 26 | /** 27 | * Implement this method to provide a block of code that uses the provided database 28 | * and is transacted. Whether the transaction will commit or rollback is typically 29 | * controlled by the code that invokes this method. 30 | * 31 | *

If a {@link Throwable} is thrown from this method, it will be caught, wrapped in 32 | * a DatabaseException (if it is not already one), and then propagated.

33 | */ 34 | T run(Supplier db, Transaction tx) throws Exception; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/Ddl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Interface for executing a chunk of DDL within the database. 21 | * 22 | * @author garricko 23 | */ 24 | public interface Ddl { 25 | /** 26 | * Execute the DDL statement. All checked SQLExceptions get wrapped in DatabaseExceptions. 27 | */ 28 | void execute(); 29 | 30 | /** 31 | * This just does an execute() call and silently discards any DatabaseException 32 | * that might occur. This can be useful for things like drop statements, where 33 | * some databases don't make it easy to conditionally drop things only if they 34 | * exist. 35 | */ 36 | void executeQuietly(); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DdlImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.sql.CallableStatement; 20 | import java.sql.Connection; 21 | import java.sql.Statement; 22 | 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | /** 27 | * This is the key class for configuring (query parameters) and executing a database query. 28 | * 29 | * @author garricko 30 | */ 31 | public class DdlImpl implements Ddl { 32 | private static final Logger log = LoggerFactory.getLogger(Database.class); 33 | private static final Logger logQuiet = LoggerFactory.getLogger(Database.class.getName() + ".Quiet"); 34 | private final Connection connection; 35 | private final String sql; 36 | private final Options options; 37 | 38 | public DdlImpl(Connection connection, String sql, Options options) { 39 | this.connection = connection; 40 | this.sql = sql; 41 | this.options = options; 42 | } 43 | 44 | private void updateInternal(boolean quiet) { 45 | CallableStatement ps = null; 46 | Metric metric = new Metric(log.isDebugEnabled()); 47 | 48 | boolean isSuccess = false; 49 | String errorCode = null; 50 | Exception logEx = null; 51 | try { 52 | ps = connection.prepareCall(sql); 53 | 54 | metric.checkpoint("prep"); 55 | ps.execute(); 56 | metric.checkpoint("exec"); 57 | isSuccess = true; 58 | } catch (Exception e) { 59 | errorCode = options.generateErrorCode(); 60 | logEx = e; 61 | throw DatabaseException.wrap(DebugSql.exceptionMessage(sql, null, errorCode, options), e); 62 | } finally { 63 | close(ps); 64 | metric.checkpoint("close"); 65 | // PostgreSQL requires explicit commit since we are running with setAutoCommit(false) 66 | commitIfNecessary(connection); 67 | metric.done("commit"); 68 | if (isSuccess) { 69 | DebugSql.logSuccess("DDL", log, metric, sql, null, options); 70 | } else if (quiet) { 71 | DebugSql.logWarning("DDL", logQuiet, metric, errorCode, sql, null, options, logEx); 72 | } else { 73 | DebugSql.logError("DDL", log, metric, errorCode, sql, null, options, logEx); 74 | } 75 | } 76 | } 77 | 78 | @Override 79 | public void execute() { 80 | updateInternal(false); 81 | } 82 | 83 | @Override 84 | public void executeQuietly() { 85 | try { 86 | updateInternal(true); 87 | } catch (DatabaseException e) { 88 | // Ignore, as requested 89 | } 90 | } 91 | 92 | private void close(Statement s) { 93 | if (s != null) { 94 | try { 95 | s.close(); 96 | } catch (Exception e) { 97 | log.warn("Caught exception closing the Statement", e); 98 | } 99 | } 100 | } 101 | 102 | private void commitIfNecessary(Connection c) { 103 | if (c != null) { 104 | try { 105 | if (!c.getAutoCommit()) { 106 | c.commit(); 107 | } 108 | } catch (Exception e) { 109 | log.warn("Caught exception on commit", e); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/DebugSql.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.io.InputStream; 20 | import java.io.Reader; 21 | import java.util.Arrays; 22 | import java.util.Date; 23 | 24 | import org.slf4j.Logger; 25 | 26 | import com.github.susom.database.MixedParameterSql.SecretArg; 27 | 28 | /** 29 | * Convenience class to substitute real values into a database query for debugging, logging, etc. 30 | *

31 | * WARNING!!! Never execute this SQL without manual inspection because this class does NOTHING 32 | * to prevent SQL injection or any other bad things. 33 | * 34 | * @author garricko 35 | */ 36 | public class DebugSql { 37 | public static final String PARAM_SQL_SEPARATOR = "\tParamSql:\t"; 38 | 39 | public static String printDebugOnlySqlString(String sql, Object[] args, Options options) { 40 | StringBuilder buf = new StringBuilder(); 41 | printSql(buf, sql, args, false, true, options); 42 | return buf.toString(); 43 | } 44 | 45 | public static void printSql(StringBuilder buf, String sql, Object[] args, Options options) { 46 | printSql(buf, sql, args, true, options.isLogParameters(), options); 47 | } 48 | 49 | public static void printSql(StringBuilder buf, String sql, Object[] args, boolean includeExecSql, 50 | boolean includeParameters, Options options) { 51 | Object[] argsToPrint = args; 52 | if (argsToPrint == null) { 53 | argsToPrint = new Object[0]; 54 | } 55 | int batchSize = -1; 56 | if (argsToPrint.length > 0 && argsToPrint instanceof Object[][]) { 57 | // The arguments provided were from a batch - just use the first set 58 | batchSize = argsToPrint.length; 59 | argsToPrint = (Object[]) argsToPrint[0]; 60 | } 61 | String[] sqlParts = sql.split("\\?"); 62 | if (sqlParts.length != argsToPrint.length + (sql.endsWith("?") ? 0 : 1)) { 63 | buf.append("(wrong # args) query: "); 64 | buf.append(sql); 65 | if (args != null) { 66 | buf.append(" args: "); 67 | if (includeParameters) { 68 | buf.append(Arrays.toString(argsToPrint)); 69 | } else { 70 | buf.append(argsToPrint.length); 71 | } 72 | } 73 | } else { 74 | if (includeExecSql) { 75 | buf.append(removeTabs(sql)); 76 | } 77 | if (includeParameters && argsToPrint.length > 0) { 78 | if (includeExecSql) { 79 | buf.append(PARAM_SQL_SEPARATOR); 80 | } 81 | for (int i = 0; i < argsToPrint.length; i++) { 82 | buf.append(removeTabs(sqlParts[i])); 83 | Object argToPrint = argsToPrint[i]; 84 | if (argToPrint instanceof String) { 85 | String argToPrintString = (String) argToPrint; 86 | int maxLength = options.maxStringLengthParam(); 87 | if (argToPrintString.length() > maxLength && maxLength > 0) { 88 | buf.append("'").append(argToPrintString.substring(0, maxLength)).append("...'"); 89 | } else { 90 | buf.append("'"); 91 | buf.append(removeTabs(escapeSingleQuoted(argToPrintString))); 92 | buf.append("'"); 93 | } 94 | } else if (argToPrint instanceof StatementAdaptor.SqlNull || argToPrint == null) { 95 | buf.append("null"); 96 | } else if (argToPrint instanceof java.sql.Timestamp) { 97 | buf.append(options.flavor().dateAsSqlFunction((Date) argToPrint, options.calendarForTimestamps())); 98 | } else if (argToPrint instanceof java.sql.Date) { 99 | buf.append(options.flavor().localDateAsSqlFunction((Date) argToPrint)); 100 | } else if (argToPrint instanceof Number) { 101 | buf.append(argToPrint); 102 | } else if (argToPrint instanceof Boolean) { 103 | buf.append(((Boolean) argToPrint) ? "'Y'" : "'N'"); 104 | } else if (argToPrint instanceof SecretArg) { 105 | buf.append(""); 106 | } else if (argToPrint instanceof InternalStringReader) { 107 | String argToPrintString = ((InternalStringReader) argToPrint).getString(); 108 | int maxLength = options.maxStringLengthParam(); 109 | if (argToPrintString.length() > maxLength && maxLength > 0) { 110 | buf.append("'").append(argToPrintString.substring(0, maxLength)).append("...'"); 111 | } else { 112 | buf.append("'"); 113 | buf.append(removeTabs(escapeSingleQuoted(argToPrintString))); 114 | buf.append("'"); 115 | } 116 | } else if (argToPrint instanceof Reader || argToPrint instanceof InputStream) { 117 | buf.append("<").append(argToPrint.getClass().getName()).append(">"); 118 | } else if (argToPrint instanceof byte[]) { 119 | buf.append("<").append(((byte[]) argToPrint).length).append(" bytes>"); 120 | } else { 121 | buf.append(""); 122 | } 123 | } 124 | if (sqlParts.length > argsToPrint.length) { 125 | buf.append(sqlParts[sqlParts.length - 1]); 126 | } 127 | } 128 | } 129 | if (batchSize != -1) { 130 | buf.append(" (first in batch of "); 131 | buf.append(batchSize); 132 | buf.append(')'); 133 | } 134 | } 135 | 136 | private static String removeTabs(String s) { 137 | return s == null ? null : s.replace("\t", ""); 138 | } 139 | 140 | private static String escapeSingleQuoted(String s) { 141 | return s == null ? null : s.replace("'", "''"); 142 | } 143 | 144 | public static String exceptionMessage(String sql, Object[] parameters, String errorCode, Options options) { 145 | StringBuilder buf = new StringBuilder("Error executing SQL"); 146 | if (errorCode != null) { 147 | buf.append(" (errorCode=").append(errorCode).append(")"); 148 | } 149 | if (options.isDetailedExceptions()) { 150 | buf.append(": "); 151 | DebugSql.printSql(buf, sql, parameters, options); 152 | } 153 | return buf.toString(); 154 | } 155 | 156 | public static void logSuccess(String sqlType, Logger log, Metric metric, String sql, Object[] args, Options options) { 157 | if (log.isDebugEnabled()) { 158 | String msg = logMiddle('\t', sqlType, metric, null, sql, args, options); 159 | log.debug(msg); 160 | } 161 | } 162 | 163 | public static void logWarning(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args, 164 | Options options, Throwable t) { 165 | if (log.isWarnEnabled()) { 166 | String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options); 167 | log.warn(msg, t); 168 | } 169 | } 170 | 171 | public static void logError(String sqlType, Logger log, Metric metric, String errorCode, String sql, Object[] args, 172 | Options options, Throwable t) { 173 | if (log.isErrorEnabled()) { 174 | String msg = logMiddle(' ', sqlType, metric, errorCode, sql, args, options); 175 | log.error(msg, t); 176 | } 177 | } 178 | 179 | private static String logMiddle(char separator, String sqlType, Metric metric, 180 | String errorCode, String sql, Object[] args, Options options) { 181 | StringBuilder buf = new StringBuilder(); 182 | if (errorCode != null) { 183 | buf.append("errorCode=").append(errorCode).append(" "); 184 | } 185 | buf.append(sqlType).append(": "); 186 | metric.printMessage(buf); 187 | buf.append(separator); 188 | printSql(buf, sql, args, options); 189 | return buf.toString(); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/InternalStringReader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | import java.io.StringReader; 19 | 20 | /** 21 | * This class exists to distinguish cases where we are mapping String to Reader 22 | * internally, but want to be able to know they really started as a String (and 23 | * be able to get the String back for things like logging). 24 | * 25 | * @author garricko 26 | */ 27 | final class InternalStringReader extends StringReader { 28 | private String s; 29 | 30 | InternalStringReader(String s) { 31 | super(s); 32 | this.s = s; 33 | } 34 | 35 | String getString() { 36 | return s; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/MixedParameterSql.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.HashSet; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Set; 25 | 26 | /** 27 | * Convenience class to allow use of (:mylabel) for SQL parameters in addition to 28 | * positional (?) parameters. This doesn't do any smart parsing of the SQL, it is just 29 | * looking for ':' and '?' characters. If the SQL needs to include an actual ':' or '?' 30 | * character, use two of them ('::' or '??'), and they will be replaced with a 31 | * single ':' or '?'. 32 | * 33 | * @author garricko 34 | */ 35 | public class MixedParameterSql { 36 | private final String sqlToExecute; 37 | private final Object[] args; 38 | 39 | public MixedParameterSql(String sql, List positionalArgs, Map nameToArg) { 40 | if (positionalArgs == null) { 41 | positionalArgs = new ArrayList<>(); 42 | } 43 | if (nameToArg == null) { 44 | nameToArg = new HashMap<>(); 45 | } 46 | 47 | StringBuilder newSql = new StringBuilder(sql.length()); 48 | List argNamesList = new ArrayList<>(); 49 | List rewrittenArgs = new ArrayList<>(); 50 | List argsList = new ArrayList<>(); 51 | int searchIndex = 0; 52 | int currentPositionalArg = 0; 53 | while (searchIndex < sql.length()) { 54 | int nextColonIndex = sql.indexOf(':', searchIndex); 55 | int nextQmIndex = sql.indexOf('?', searchIndex); 56 | 57 | if (nextColonIndex < 0 && nextQmIndex < 0) { 58 | newSql.append(sql.substring(searchIndex)); 59 | break; 60 | } 61 | 62 | if (nextColonIndex >= 0 && (nextQmIndex == -1 || nextColonIndex < nextQmIndex)) { 63 | // The next parameter we found is a named parameter (":foo") 64 | if (nextColonIndex > sql.length() - 2) { 65 | // Probably illegal sql, but handle boundary condition 66 | break; 67 | } 68 | 69 | // Allow :: as escape for : 70 | if (sql.charAt(nextColonIndex + 1) == ':') { 71 | newSql.append(sql.substring(searchIndex, nextColonIndex + 1)); 72 | searchIndex = nextColonIndex + 2; 73 | continue; 74 | } 75 | 76 | int endOfNameIndex = nextColonIndex + 1; 77 | while (endOfNameIndex < sql.length() && Character.isJavaIdentifierPart(sql.charAt(endOfNameIndex))) { 78 | endOfNameIndex++; 79 | } 80 | newSql.append(sql.substring(searchIndex, nextColonIndex)); 81 | String paramName = sql.substring(nextColonIndex + 1, endOfNameIndex); 82 | boolean secretParam = paramName.startsWith("secret"); 83 | Object arg = nameToArg.get(paramName); 84 | if (arg instanceof RewriteArg) { 85 | newSql.append(((RewriteArg) arg).sql); 86 | rewrittenArgs.add(paramName); 87 | } else { 88 | newSql.append('?'); 89 | if (nameToArg.containsKey(paramName)) { 90 | argsList.add(secretParam ? new SecretArg(arg): arg); 91 | } else { 92 | throw new DatabaseException("The SQL requires parameter ':" + paramName + "' but no value was provided"); 93 | } 94 | argNamesList.add(paramName); 95 | } 96 | searchIndex = endOfNameIndex; 97 | } else { 98 | // The next parameter we found is a positional parameter ("?") 99 | 100 | // Allow ?? as escape for ? 101 | if (nextQmIndex < sql.length() - 1 && sql.charAt(nextQmIndex + 1) == '?') { 102 | newSql.append(sql.substring(searchIndex, nextQmIndex + 1)); 103 | searchIndex = nextQmIndex + 2; 104 | continue; 105 | } 106 | 107 | newSql.append(sql.substring(searchIndex, nextQmIndex)); 108 | if (currentPositionalArg >= positionalArgs.size()) { 109 | throw new DatabaseException("Not enough positional parameters (" + positionalArgs.size() + ") were provided"); 110 | } 111 | if (positionalArgs.get(currentPositionalArg) instanceof RewriteArg) { 112 | newSql.append(((RewriteArg) positionalArgs.get(currentPositionalArg)).sql); 113 | } else { 114 | newSql.append('?'); 115 | argsList.add(positionalArgs.get(currentPositionalArg)); 116 | } 117 | currentPositionalArg++; 118 | searchIndex = nextQmIndex + 1; 119 | } 120 | } 121 | 122 | this.sqlToExecute = newSql.toString(); 123 | args = argsList.toArray(new Object[argsList.size()]); 124 | 125 | // Sanity check number of arguments to provide a better error message 126 | if (currentPositionalArg != positionalArgs.size()) { 127 | throw new DatabaseException("Wrong number of positional parameters were provided (expected: " 128 | + currentPositionalArg + ", actual: " + positionalArgs.size() + ")"); 129 | } 130 | if (nameToArg.size() > args.length - Math.max(0, positionalArgs.size() - 1) + rewrittenArgs.size()) { 131 | Set unusedNames = new HashSet<>(nameToArg.keySet()); 132 | unusedNames.removeAll(argNamesList); 133 | unusedNames.removeAll(rewrittenArgs); 134 | if (!unusedNames.isEmpty()) { 135 | throw new DatabaseException("These named parameters do not exist in the query: " + unusedNames); 136 | } 137 | } 138 | } 139 | 140 | public String getSqlToExecute() { 141 | return sqlToExecute; 142 | } 143 | 144 | public Object[] getArgs() { 145 | return args; 146 | } 147 | 148 | public static class RewriteArg { 149 | private final String sql; 150 | 151 | public RewriteArg(String sql) { 152 | this.sql = sql; 153 | } 154 | } 155 | 156 | static class SecretArg { 157 | private final Object arg; 158 | 159 | SecretArg(Object arg) { 160 | this.arg = arg; 161 | } 162 | 163 | Object getArg() { 164 | return arg; 165 | } 166 | 167 | @Override 168 | public String toString() { 169 | return ""; 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/Options.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.util.Calendar; 20 | import java.util.Date; 21 | 22 | /** 23 | * Control various optional behavior for the database interactions. 24 | * 25 | * @author garricko 26 | */ 27 | public interface Options { 28 | /** 29 | * Control whether the Database object will allow calls to commitNow() 30 | * and rollbackNow(). By default it will throw exceptions if you try to 31 | * call those. 32 | */ 33 | boolean allowTransactionControl(); 34 | 35 | /** 36 | * Useful for testing code that explicitly controls transactions, and you 37 | * don't really want it to commit/rollback. Disabled by default, meaning 38 | * calls will be allowed or throw exceptions depending on allowTransctionControl(). 39 | * The value of allowTranscationControl() has no affect if this returns true. 40 | */ 41 | boolean ignoreTransactionControl(); 42 | 43 | /** 44 | * Control whether the Database object will allow calls to underlyingConnection(). 45 | * By default that method will throw an exception. 46 | */ 47 | boolean allowConnectionAccess(); 48 | 49 | /** 50 | * If this is false, log messages will look something like: 51 | * 52 | *
 53 |    *   ...select a from b where c=?
 54 |    * 
55 | * 56 | * If this is true, log messages will look something like: 57 | * 58 | *
 59 |    *   ...select a from b where c=?|select a from b where c='abc'
 60 |    * 
61 | * 62 | * @return true if parameter values should be logged along with SQL, false otherwise 63 | */ 64 | boolean isLogParameters(); 65 | 66 | /** 67 | * If true, text of the SQL and possibly parameter values (depending on @{#isLogParameters()}) 68 | * will be included in exception messages. This can be very helpful for debugging, but poses 69 | * some disclosure risks. 70 | * 71 | * @return true to add possibly sensitive data in exception messages, false otherwise 72 | */ 73 | boolean isDetailedExceptions(); 74 | 75 | /** 76 | * In cases where exceptions are thrown, use this method to provide a common 77 | * code that will be included in the exception message and the log message 78 | * so they can be searched and correlated later. 79 | * 80 | * @return an arbitrary, fairly unique, speakable over the phone, without whitespace 81 | */ 82 | String generateErrorCode(); 83 | 84 | /** 85 | * Indicate whether to use the Blob functionality of the underlying database driver, 86 | * or whether to use setBytes() methods instead. Using Blobs is preferred, but is not 87 | * supported by all drivers. 88 | * 89 | *

The default behavior of this method is to delegate to flavor().useBytesForBlob(), 90 | * but it is provided on this interface so the behavior can be controlled. 91 | * 92 | * @return true to avoid using Blob functionality, false otherwise 93 | */ 94 | boolean useBytesForBlob(); 95 | 96 | /** 97 | * Indicate whether to use the Clob functionality of the underlying database driver, 98 | * or whether to use setString() methods instead. Using Clobs is preferred, but is not 99 | * supported by all drivers. 100 | * 101 | *

The default behavior of this method is to delegate to flavor().useStringForClob(), 102 | * but it is provided on this interface so the behavior can be controlled. 103 | * 104 | * @return true to avoid using Clob functionality, false otherwise 105 | */ 106 | boolean useStringForClob(); 107 | 108 | /** 109 | * Access compatibility information for the underlying database. The 110 | * Flavor class enumerates the known databases and tries to smooth over 111 | * some of the variations in features and syntax. 112 | */ 113 | Flavor flavor(); 114 | 115 | /** 116 | * The value returned by this method will be used for argDateNowPerApp() calls. It 117 | * may also be used for argDateNowPerDb() calls if you have enabled that. 118 | */ 119 | Date currentDate(); 120 | 121 | /** 122 | * Wherever argDateNowPerDb() is specified, use argDateNowPerApp() instead. This is 123 | * useful for testing purposes as you can use OptionsOverride to provide your 124 | * own system clock that will be used for time travel. 125 | */ 126 | boolean useDatePerAppOnly(); 127 | 128 | /** 129 | * This calendar will be used for conversions when storing and retrieving timestamps 130 | * from the database. By default this is the JVM default with TimeZone explicitly set 131 | * to GMT (so timestamps will be stored in the database as GMT). 132 | * 133 | *

It is strongly recommended to always run your database in GMT timezone, and 134 | * leave this set to the default.

135 | * 136 | *

Behavior in releases 1.3 and prior was to use the JVM default TimeZone, and 137 | * this was not configurable.

138 | */ 139 | Calendar calendarForTimestamps(); 140 | 141 | /** 142 | * The maximum number of characters to print in debug SQL for a given String type 143 | * insert/update/query parameter. If it exceeds this length, the parameter value 144 | * will be truncated at the max and a "..." will be appended. Note this affects 145 | * both {@code argString()} and {@code argClobString()} methods. 146 | */ 147 | int maxStringLengthParam(); 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/OptionsDefault.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.text.SimpleDateFormat; 20 | import java.util.Calendar; 21 | import java.util.Date; 22 | import java.util.TimeZone; 23 | 24 | /** 25 | * Control various optional behavior for the database interactions. 26 | * 27 | * @author garricko 28 | */ 29 | public class OptionsDefault implements Options { 30 | private final Flavor flavor; 31 | 32 | public OptionsDefault(Flavor flavor) { 33 | this.flavor = flavor; 34 | } 35 | 36 | @Override 37 | public boolean allowTransactionControl() { 38 | return false; 39 | } 40 | 41 | @Override 42 | public boolean ignoreTransactionControl() { 43 | return false; 44 | } 45 | 46 | @Override 47 | public boolean allowConnectionAccess() { 48 | return false; 49 | } 50 | 51 | @Override 52 | public boolean isLogParameters() { 53 | return false; 54 | } 55 | 56 | @Override 57 | public boolean isDetailedExceptions() { 58 | return false; 59 | } 60 | 61 | @Override 62 | public String generateErrorCode() { 63 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd:H:m:s"); 64 | return sdf.format(new Date()) + "-" + Math.round(Math.random() * 1000000); 65 | } 66 | 67 | @Override 68 | public boolean useBytesForBlob() { 69 | return flavor().useBytesForBlob(); 70 | } 71 | 72 | @Override 73 | public boolean useStringForClob() { 74 | return flavor().useStringForClob(); 75 | } 76 | 77 | @Override 78 | public Flavor flavor() { 79 | return flavor; 80 | } 81 | 82 | @Override 83 | public Date currentDate() { 84 | return new Date(); 85 | } 86 | 87 | @Override 88 | public boolean useDatePerAppOnly() { 89 | return false; 90 | } 91 | 92 | @Override 93 | public Calendar calendarForTimestamps() { 94 | return Calendar.getInstance(TimeZone.getDefault()); 95 | } 96 | 97 | @Override 98 | public int maxStringLengthParam() { 99 | return 4000; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/OptionsOverride.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.util.Calendar; 20 | import java.util.Date; 21 | 22 | /** 23 | * Base class for selectively overriding another Options object. 24 | * 25 | * @author garricko 26 | */ 27 | public class OptionsOverride implements Options { 28 | private Options parent; 29 | 30 | /** 31 | * Wrap another {@code Options} and defer to it for anything we choose not 32 | * to override. 33 | */ 34 | public OptionsOverride(Options parent) { 35 | this.parent = parent; 36 | } 37 | 38 | /** 39 | * Defer to OptionsDefault for anything that is not specified, and use postgresql flavor. 40 | */ 41 | public OptionsOverride() { 42 | parent = new OptionsDefault(Flavor.postgresql); 43 | } 44 | 45 | /** 46 | * Defer to OptionsDefault for anything that is not specified, using the specified flavor. 47 | */ 48 | public OptionsOverride(Flavor flavor) { 49 | parent = new OptionsDefault(flavor); 50 | } 51 | 52 | public void setParent(Options parent) { 53 | this.parent = parent; 54 | } 55 | 56 | public OptionsOverride withParent(Options parent) { 57 | this.parent = parent; 58 | return this; 59 | } 60 | 61 | @Override 62 | public boolean allowTransactionControl() { 63 | return parent.allowTransactionControl(); 64 | } 65 | 66 | @Override 67 | public boolean ignoreTransactionControl() { 68 | return parent.ignoreTransactionControl(); 69 | } 70 | 71 | @Override 72 | public boolean allowConnectionAccess() { 73 | return parent.allowConnectionAccess(); 74 | } 75 | 76 | @Override 77 | public boolean isLogParameters() { 78 | return parent.isLogParameters(); 79 | } 80 | 81 | @Override 82 | public boolean isDetailedExceptions() { 83 | return parent.isDetailedExceptions(); 84 | } 85 | 86 | @Override 87 | public String generateErrorCode() { 88 | return parent.generateErrorCode(); 89 | } 90 | 91 | @Override 92 | public boolean useBytesForBlob() { 93 | return parent.useBytesForBlob(); 94 | } 95 | 96 | @Override 97 | public boolean useStringForClob() { 98 | return parent.useStringForClob(); 99 | } 100 | 101 | @Override 102 | public Flavor flavor() { 103 | return parent.flavor(); 104 | } 105 | 106 | @Override 107 | public Date currentDate() { 108 | return parent.currentDate(); 109 | } 110 | 111 | @Override 112 | public boolean useDatePerAppOnly() { 113 | return parent.useDatePerAppOnly(); 114 | } 115 | 116 | @Override 117 | public Calendar calendarForTimestamps() { 118 | return parent.calendarForTimestamps(); 119 | } 120 | 121 | @Override 122 | public int maxStringLengthParam() { 123 | return parent.maxStringLengthParam(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/QueryTimedOutException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Thrown when a query is interrupted because a timeout was exceeded or it was 21 | * explicitly cancelled. 22 | * 23 | * @author garricko 24 | */ 25 | public class QueryTimedOutException extends DatabaseException { 26 | public QueryTimedOutException(String message, Throwable cause) { 27 | super(message, cause); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/RowHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Type-safe callback to read query results. 21 | * 22 | * @author garricko 23 | */ 24 | public interface RowHandler { 25 | T process(Row r) throws Exception; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/Rows.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Interface for reading results from a database query. 21 | * 22 | * @author garricko 23 | */ 24 | public interface Rows extends Row { 25 | boolean next(); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/RowsHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Type-safe callback to read query results. 21 | * 22 | * @author garricko 23 | */ 24 | public interface RowsHandler { 25 | T process(Rows rs) throws Exception; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/SqlInsert.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.io.InputStream; 20 | import java.io.Reader; 21 | import java.math.BigDecimal; 22 | import java.time.LocalDate; 23 | import java.util.Date; 24 | 25 | import javax.annotation.CheckReturnValue; 26 | import javax.annotation.Nonnull; 27 | 28 | /** 29 | * Interface for configuring (setting parameters) and executing a chunk of SQL. 30 | * 31 | * @author garricko 32 | */ 33 | public interface SqlInsert { 34 | @Nonnull 35 | @CheckReturnValue 36 | SqlInsert argBoolean(Boolean arg); 37 | 38 | @Nonnull 39 | @CheckReturnValue 40 | SqlInsert argBoolean(@Nonnull String argName, Boolean arg); 41 | 42 | @Nonnull 43 | @CheckReturnValue 44 | SqlInsert argInteger(Integer arg); 45 | 46 | @Nonnull 47 | @CheckReturnValue 48 | SqlInsert argInteger(@Nonnull String argName, Integer arg); 49 | 50 | @Nonnull 51 | @CheckReturnValue 52 | SqlInsert argLong(Long arg); 53 | 54 | @Nonnull 55 | @CheckReturnValue 56 | SqlInsert argLong(@Nonnull String argName, Long arg); 57 | 58 | @Nonnull 59 | @CheckReturnValue 60 | SqlInsert argFloat(Float arg); 61 | 62 | @Nonnull 63 | @CheckReturnValue 64 | SqlInsert argFloat(@Nonnull String argName, Float arg); 65 | 66 | @Nonnull 67 | @CheckReturnValue 68 | SqlInsert argDouble(Double arg); 69 | 70 | @Nonnull 71 | @CheckReturnValue 72 | SqlInsert argDouble(@Nonnull String argName, Double arg); 73 | 74 | @Nonnull 75 | @CheckReturnValue 76 | SqlInsert argBigDecimal(BigDecimal arg); 77 | 78 | @Nonnull 79 | @CheckReturnValue 80 | SqlInsert argBigDecimal(@Nonnull String argName, BigDecimal arg); 81 | 82 | @Nonnull 83 | @CheckReturnValue 84 | SqlInsert argString(String arg); 85 | 86 | @Nonnull 87 | @CheckReturnValue 88 | SqlInsert argString(@Nonnull String argName, String arg); 89 | 90 | @Nonnull 91 | @CheckReturnValue 92 | SqlInsert argDate(Date arg); // date with time 93 | 94 | @Nonnull 95 | @CheckReturnValue 96 | SqlInsert argDate(@Nonnull String argName, Date arg); // date with time 97 | 98 | @Nonnull 99 | @CheckReturnValue 100 | SqlInsert argLocalDate(LocalDate arg); // date only - no timestamp 101 | 102 | @Nonnull 103 | @CheckReturnValue 104 | SqlInsert argLocalDate(@Nonnull String argName, LocalDate arg); // date only - no timestamp 105 | 106 | @Nonnull 107 | @CheckReturnValue 108 | SqlInsert argDateNowPerApp(); 109 | 110 | @Nonnull 111 | @CheckReturnValue 112 | SqlInsert argDateNowPerApp(@Nonnull String argName); 113 | 114 | @Nonnull 115 | @CheckReturnValue 116 | SqlInsert argDateNowPerDb(); 117 | 118 | @Nonnull 119 | @CheckReturnValue 120 | SqlInsert argDateNowPerDb(@Nonnull String argName); 121 | 122 | @Nonnull 123 | @CheckReturnValue 124 | SqlInsert argBlobBytes(byte[] arg); 125 | 126 | @Nonnull 127 | @CheckReturnValue 128 | SqlInsert argBlobBytes(@Nonnull String argName, byte[] arg); 129 | 130 | @Nonnull 131 | @CheckReturnValue 132 | SqlInsert argBlobStream(InputStream arg); 133 | 134 | @Nonnull 135 | @CheckReturnValue 136 | SqlInsert argBlobStream(@Nonnull String argName, InputStream arg); 137 | 138 | @Nonnull 139 | @CheckReturnValue 140 | SqlInsert argClobString(String arg); 141 | 142 | @Nonnull 143 | @CheckReturnValue 144 | SqlInsert argClobString(@Nonnull String argName, String arg); 145 | 146 | @Nonnull 147 | @CheckReturnValue 148 | SqlInsert argClobReader(Reader arg); 149 | 150 | @Nonnull 151 | @CheckReturnValue 152 | SqlInsert argClobReader(@Nonnull String argName, Reader arg); 153 | 154 | interface Apply { 155 | void apply(SqlInsert insert); 156 | } 157 | 158 | @Nonnull 159 | @CheckReturnValue 160 | SqlInsert withArgs(SqlArgs args); 161 | 162 | @Nonnull 163 | @CheckReturnValue 164 | SqlInsert apply(Apply apply); 165 | 166 | /** 167 | * Call this between setting rows of parameters for a SQL statement. You may call it before 168 | * setting any parameters, after setting all, or multiple times between rows. This feature 169 | * only currently works with basic inserts (you can't do insertReturning type operations). 170 | */ 171 | SqlInsert batch(); 172 | 173 | /** 174 | * Perform the insert into the database without any verification of how many rows 175 | * were affected. 176 | * 177 | * @return the number of rows affected 178 | */ 179 | int insert(); 180 | 181 | /** 182 | * Perform the insert into the database. This will automatically verify 183 | * that the specified number of rows was affected, and throw a {@link WrongNumberOfRowsException} 184 | * if it does not match. 185 | */ 186 | void insert(int expectedRowsUpdated); 187 | 188 | /** 189 | * Insert multiple rows in one database call. This will automatically verify 190 | * that exactly 1 row is affected for each row of parameters. 191 | */ 192 | void insertBatch(); 193 | 194 | /** 195 | * Insert multiple rows in one database call. This returns the results for 196 | * each row so you can check them yourself. 197 | * 198 | * @return an array with an element for each row in the batch; the value 199 | * of each array indicates how many rows were affected; note that 200 | * some database/driver combinations do now return this information 201 | * (for example, older versions of Oracle return -2 rather than the 202 | * number of rows) 203 | */ 204 | int[] insertBatchUnchecked(); 205 | 206 | /** 207 | * Use this method in conjunction with argPkSeq() to optimize inserts where the 208 | * primary key is being populated from a database sequence at insert time. If the 209 | * database can't support this feature it will be simulated with a select and then 210 | * the insert. 211 | * 212 | *

This version of insert expects exactly one row to be inserted, and will throw 213 | * a DatabaseException if that isn't the case.

214 | */ 215 | @CheckReturnValue 216 | Long insertReturningPkSeq(String primaryKeyColumnName); 217 | 218 | T insertReturning(String tableName, String primaryKeyColumnName, RowsHandler rowsHandler, 219 | String...otherColumnNames); 220 | 221 | @Nonnull 222 | @CheckReturnValue 223 | SqlInsert argPkSeq(@Nonnull String sequenceName); 224 | 225 | /** 226 | * Use this method to populate the primary key value (assumed to be type Long) 227 | * from a sequence in the database. This can be used standalone, but is intended 228 | * to be used in conjunction with insertReturningPkSeq() to both insert and obtain 229 | * the inserted value in an optimized way (if possible). For databases that are 230 | * unable to return the value from the insert (such as Derby) this will be simulated 231 | * first issuing a select to read the sequence, then an insert. 232 | */ 233 | @Nonnull 234 | @CheckReturnValue 235 | SqlInsert argPkSeq(@Nonnull String argName, @Nonnull String sequenceName); 236 | 237 | @Nonnull 238 | @CheckReturnValue 239 | SqlInsert argPkLong(Long pkValue); 240 | 241 | @Nonnull 242 | @CheckReturnValue 243 | SqlInsert argPkLong(String argName, Long pkValue); 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/SqlSelect.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.math.BigDecimal; 20 | import java.time.LocalDate; 21 | import java.util.Date; 22 | import java.util.List; 23 | 24 | import javax.annotation.CheckReturnValue; 25 | import javax.annotation.Nonnull; 26 | import javax.annotation.Nullable; 27 | 28 | /** 29 | * Interface for configuring (setting parameters) and executing a chunk of SQL. 30 | * 31 | * @author garricko 32 | */ 33 | public interface SqlSelect { 34 | @Nonnull 35 | @CheckReturnValue 36 | SqlSelect argBoolean(Boolean arg); 37 | 38 | @Nonnull 39 | @CheckReturnValue 40 | SqlSelect argBoolean(@Nonnull String argName, Boolean arg); 41 | 42 | @Nonnull 43 | @CheckReturnValue 44 | SqlSelect argInteger(Integer arg); 45 | 46 | @Nonnull 47 | @CheckReturnValue 48 | SqlSelect argInteger(@Nonnull String argName, Integer arg); 49 | 50 | @Nonnull 51 | @CheckReturnValue 52 | SqlSelect argLong(Long arg); 53 | 54 | @Nonnull 55 | @CheckReturnValue 56 | SqlSelect argLong(@Nonnull String argName, Long arg); 57 | 58 | @Nonnull 59 | @CheckReturnValue 60 | SqlSelect argFloat(Float arg); 61 | 62 | @Nonnull 63 | @CheckReturnValue 64 | SqlSelect argFloat(@Nonnull String argName, Float arg); 65 | 66 | @Nonnull 67 | @CheckReturnValue 68 | SqlSelect argDouble(Double arg); 69 | 70 | @Nonnull 71 | @CheckReturnValue 72 | SqlSelect argDouble(@Nonnull String argName, Double arg); 73 | 74 | @Nonnull 75 | @CheckReturnValue 76 | SqlSelect argBigDecimal(BigDecimal arg); 77 | 78 | @Nonnull 79 | @CheckReturnValue 80 | SqlSelect argBigDecimal(@Nonnull String argName, BigDecimal arg); 81 | 82 | @Nonnull 83 | @CheckReturnValue 84 | SqlSelect argString(String arg); 85 | 86 | @Nonnull 87 | @CheckReturnValue 88 | SqlSelect argString(@Nonnull String argName, String arg); 89 | 90 | @Nonnull 91 | @CheckReturnValue 92 | SqlSelect argDate(Date arg); // Date with time 93 | 94 | @Nonnull 95 | @CheckReturnValue 96 | SqlSelect argDate(@Nonnull String argName, Date arg); // Date with time 97 | 98 | @Nonnull 99 | @CheckReturnValue 100 | SqlSelect argLocalDate(LocalDate arg); // Date without time 101 | 102 | @Nonnull 103 | @CheckReturnValue 104 | SqlSelect argLocalDate(@Nonnull String argName, LocalDate arg); // Date without time 105 | 106 | @Nonnull 107 | @CheckReturnValue 108 | SqlSelect argDateNowPerApp(); 109 | 110 | @Nonnull 111 | @CheckReturnValue 112 | SqlSelect argDateNowPerApp(@Nonnull String argName); 113 | 114 | @Nonnull 115 | @CheckReturnValue 116 | SqlSelect argDateNowPerDb(); 117 | 118 | @Nonnull 119 | @CheckReturnValue 120 | SqlSelect argDateNowPerDb(@Nonnull String argName); 121 | 122 | @Nonnull 123 | @CheckReturnValue 124 | SqlSelect withTimeoutSeconds(int seconds); 125 | 126 | @Nonnull 127 | @CheckReturnValue 128 | SqlSelect withMaxRows(int rows); 129 | 130 | interface Apply { 131 | void apply(SqlSelect select); 132 | } 133 | 134 | @Nonnull 135 | @CheckReturnValue 136 | SqlSelect withArgs(SqlArgs args); 137 | 138 | @Nonnull 139 | @CheckReturnValue 140 | SqlSelect apply(Apply apply); 141 | 142 | @Nonnull 143 | @CheckReturnValue 144 | SqlSelect fetchSize(int fetchSize); 145 | 146 | @Nullable 147 | @CheckReturnValue 148 | Boolean queryBooleanOrNull(); 149 | 150 | @CheckReturnValue 151 | boolean queryBooleanOrFalse(); 152 | 153 | @CheckReturnValue 154 | boolean queryBooleanOrTrue(); 155 | 156 | @Nullable 157 | @CheckReturnValue 158 | Long queryLongOrNull(); 159 | 160 | @CheckReturnValue 161 | long queryLongOrZero(); 162 | 163 | /** 164 | * Shorthand for reading numbers from the first column of the result. 165 | * 166 | * @return the first column values, omitting any that were null 167 | */ 168 | @Nonnull 169 | @CheckReturnValue 170 | List queryLongs(); 171 | 172 | @Nullable 173 | @CheckReturnValue 174 | Integer queryIntegerOrNull(); 175 | 176 | @CheckReturnValue 177 | int queryIntegerOrZero(); 178 | 179 | @Nonnull 180 | @CheckReturnValue 181 | List queryIntegers(); 182 | 183 | @Nullable 184 | @CheckReturnValue 185 | Float queryFloatOrNull(); 186 | 187 | @CheckReturnValue 188 | float queryFloatOrZero(); 189 | 190 | @Nonnull 191 | @CheckReturnValue 192 | List queryFloats(); 193 | 194 | @Nullable 195 | @CheckReturnValue 196 | Double queryDoubleOrNull(); 197 | 198 | @CheckReturnValue 199 | double queryDoubleOrZero(); 200 | 201 | @Nonnull 202 | @CheckReturnValue 203 | List queryDoubles(); 204 | 205 | @Nullable 206 | @CheckReturnValue 207 | BigDecimal queryBigDecimalOrNull(); 208 | 209 | @Nonnull 210 | @CheckReturnValue 211 | BigDecimal queryBigDecimalOrZero(); 212 | 213 | @Nonnull 214 | @CheckReturnValue 215 | List queryBigDecimals(); 216 | 217 | @Nullable 218 | @CheckReturnValue 219 | String queryStringOrNull(); 220 | 221 | @Nonnull 222 | @CheckReturnValue 223 | String queryStringOrEmpty(); 224 | 225 | /** 226 | * Shorthand for reading strings from the first column of the result. 227 | * 228 | * @return the first column values, omitting any that were null 229 | */ 230 | @Nonnull 231 | @CheckReturnValue 232 | List queryStrings(); 233 | 234 | @Nullable 235 | @CheckReturnValue 236 | Date queryDateOrNull(); // Date with time 237 | 238 | @Nonnull 239 | @CheckReturnValue 240 | List queryDates(); // Date with time 241 | 242 | @Nullable 243 | @CheckReturnValue 244 | LocalDate queryLocalDateOrNull(); // Date without time 245 | 246 | @Nonnull 247 | @CheckReturnValue 248 | List queryLocalDates(); // Date without time 249 | 250 | /** 251 | * This is the most generic and low-level way to iterate the query results. 252 | * Consider using one of the other methods that can handle the iteration for you. 253 | * 254 | * @param rowsHandler the process() method of this handler will be called once 255 | * and it will be responsible for iterating the results 256 | */ 257 | T query(RowsHandler rowsHandler); 258 | 259 | /** 260 | * Query zero or one row. If zero rows are available a null will be returned. 261 | * If more than one row is available a {@link ConstraintViolationException} 262 | * will be thrown. 263 | * 264 | * @param rowHandler the process() method of this handler will be called once 265 | * if there are results, or will not be called if there are 266 | * no results 267 | */ 268 | T queryOneOrNull(RowHandler rowHandler); 269 | 270 | /** 271 | * Query exactly one row. If zero rows are available or more than one row is 272 | * available a {@link ConstraintViolationException} will be thrown. 273 | * 274 | * @param rowHandler the process() method of this handler will be called once 275 | * if there are results, or will not be called if there are 276 | * no results 277 | */ 278 | T queryOneOrThrow(RowHandler rowHandler); 279 | 280 | /** 281 | * Query zero or one row. If zero rows are available a null will be returned. 282 | * If more than one row is available the first row will be returned. 283 | * 284 | * @param rowHandler the process() method of this handler will be called once 285 | * if there are results (for the first row), or will not be 286 | * called if there are no results 287 | */ 288 | T queryFirstOrNull(RowHandler rowHandler); 289 | 290 | /** 291 | * Query zero or one row. If zero rows are available a {@link ConstraintViolationException} 292 | * will be thrown. If more than one row is available the first row will be returned. 293 | * 294 | * @param rowHandler the process() method of this handler will be called once 295 | * if there are results (for the first row), or will not be 296 | * called if there are no results 297 | */ 298 | T queryFirstOrThrow(RowHandler rowHandler); 299 | 300 | /** 301 | * Query zero or more rows. If zero rows are available an empty list will be returned. 302 | * If one or more rows are available each row will be read and added to a list, which 303 | * is returned. 304 | * 305 | * @param rowHandler the process() method of this handler will be called once 306 | * for each row in the result, or will not be called if there are 307 | * no results. Only non-null values returned will be added to the 308 | * result list. 309 | */ 310 | List queryMany(RowHandler rowHandler); 311 | } 312 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/SqlUpdate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.io.InputStream; 20 | import java.io.Reader; 21 | import java.math.BigDecimal; 22 | import java.time.LocalDate; 23 | import java.util.Date; 24 | 25 | import javax.annotation.CheckReturnValue; 26 | import javax.annotation.Nonnull; 27 | import javax.annotation.Nullable; 28 | 29 | /** 30 | * Interface for configuring (setting parameters) and executing a chunk of SQL. 31 | * 32 | * @author garricko 33 | */ 34 | public interface SqlUpdate { 35 | @Nonnull 36 | @CheckReturnValue 37 | SqlUpdate argBoolean(Boolean arg); 38 | 39 | @Nonnull 40 | @CheckReturnValue 41 | SqlUpdate argBoolean(@Nonnull String argName, Boolean arg); 42 | 43 | @Nonnull 44 | @CheckReturnValue 45 | SqlUpdate argInteger(@Nullable Integer arg); 46 | 47 | @Nonnull 48 | @CheckReturnValue 49 | SqlUpdate argInteger(@Nonnull String argName, @Nullable Integer arg); 50 | 51 | @Nonnull 52 | @CheckReturnValue 53 | SqlUpdate argLong(@Nullable Long arg); 54 | 55 | @Nonnull 56 | @CheckReturnValue 57 | SqlUpdate argLong(@Nonnull String argName, @Nullable Long arg); 58 | 59 | @Nonnull 60 | @CheckReturnValue 61 | SqlUpdate argFloat(@Nullable Float arg); 62 | 63 | @Nonnull 64 | @CheckReturnValue 65 | SqlUpdate argFloat(@Nonnull String argName, @Nullable Float arg); 66 | 67 | @Nonnull 68 | @CheckReturnValue 69 | SqlUpdate argDouble(@Nullable Double arg); 70 | 71 | @Nonnull 72 | @CheckReturnValue 73 | SqlUpdate argDouble(@Nonnull String argName, @Nullable Double arg); 74 | 75 | @Nonnull 76 | @CheckReturnValue 77 | SqlUpdate argBigDecimal(@Nullable BigDecimal arg); 78 | 79 | @Nonnull 80 | @CheckReturnValue 81 | SqlUpdate argBigDecimal(@Nonnull String argName, @Nullable BigDecimal arg); 82 | 83 | @Nonnull 84 | @CheckReturnValue 85 | SqlUpdate argString(@Nullable String arg); 86 | 87 | @Nonnull 88 | @CheckReturnValue 89 | SqlUpdate argString(@Nonnull String argName, @Nullable String arg); 90 | 91 | @Nonnull 92 | @CheckReturnValue 93 | SqlUpdate argDate(@Nullable Date arg); // Date with timestamp 94 | 95 | @Nonnull 96 | @CheckReturnValue 97 | SqlUpdate argDate(@Nonnull String argName, @Nullable Date arg); // Date with timestamp 98 | 99 | @Nonnull 100 | @CheckReturnValue 101 | SqlUpdate argLocalDate(@Nullable LocalDate arg); // Date only - no timestamp 102 | 103 | @Nonnull 104 | @CheckReturnValue 105 | SqlUpdate argLocalDate(@Nonnull String argName, @Nullable LocalDate arg); // Date only - no timestamp 106 | 107 | @Nonnull 108 | @CheckReturnValue 109 | SqlUpdate argDateNowPerApp(); 110 | 111 | @Nonnull 112 | @CheckReturnValue 113 | SqlUpdate argDateNowPerApp(@Nonnull String argName); 114 | 115 | @Nonnull 116 | @CheckReturnValue 117 | SqlUpdate argDateNowPerDb(); 118 | 119 | @Nonnull 120 | @CheckReturnValue 121 | SqlUpdate argDateNowPerDb(@Nonnull String argName); 122 | 123 | @Nonnull 124 | @CheckReturnValue 125 | SqlUpdate argBlobBytes(@Nullable byte[] arg); 126 | 127 | @Nonnull 128 | @CheckReturnValue 129 | SqlUpdate argBlobBytes(@Nonnull String argName, @Nullable byte[] arg); 130 | 131 | @Nonnull 132 | @CheckReturnValue 133 | SqlUpdate argBlobStream(@Nullable InputStream arg); 134 | 135 | @Nonnull 136 | @CheckReturnValue 137 | SqlUpdate argBlobStream(@Nonnull String argName, @Nullable InputStream arg); 138 | 139 | @Nonnull 140 | @CheckReturnValue 141 | SqlUpdate argClobString(@Nullable String arg); 142 | 143 | @Nonnull 144 | @CheckReturnValue 145 | SqlUpdate argClobString(@Nonnull String argName, @Nullable String arg); 146 | 147 | @Nonnull 148 | @CheckReturnValue 149 | SqlUpdate argClobReader(@Nullable Reader arg); 150 | 151 | @Nonnull 152 | @CheckReturnValue 153 | SqlUpdate argClobReader(@Nonnull String argName, @Nullable Reader arg); 154 | 155 | /** 156 | * Call this between setting rows of parameters for a SQL statement. You may call it before 157 | * setting any parameters, after setting all, or multiple times between rows. 158 | */ 159 | // SqlUpdate batch(); 160 | 161 | // SqlUpdate withTimeoutSeconds(int seconds); 162 | 163 | interface Apply { 164 | void apply(SqlUpdate update); 165 | } 166 | 167 | @Nonnull 168 | @CheckReturnValue 169 | SqlUpdate withArgs(SqlArgs args); 170 | 171 | @Nonnull 172 | @CheckReturnValue 173 | SqlUpdate apply(Apply apply); 174 | 175 | /** 176 | * Execute the SQL update and return the number of rows was affected. 177 | */ 178 | int update(); 179 | 180 | /** 181 | * Execute the SQL update and check that the expected number of rows was affected. 182 | * 183 | * @throws WrongNumberOfRowsException if the number of rows affected did not match 184 | * the value provided 185 | */ 186 | void update(int expectedRowsUpdated); 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/StatementAdaptor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.Reader; 23 | import java.sql.ParameterMetaData; 24 | import java.sql.PreparedStatement; 25 | import java.sql.ResultSet; 26 | import java.sql.SQLException; 27 | import java.sql.Statement; 28 | import java.sql.Timestamp; 29 | import java.sql.Types; 30 | import java.time.LocalDate; 31 | import java.util.Date; 32 | import java.util.Scanner; 33 | 34 | import javax.annotation.Nullable; 35 | 36 | import org.slf4j.Logger; 37 | 38 | import com.github.susom.database.MixedParameterSql.SecretArg; 39 | 40 | import oracle.jdbc.OraclePreparedStatement; 41 | 42 | /** 43 | * Deal with mapping parameters into prepared statements. 44 | * 45 | * @author garricko 46 | */ 47 | public class StatementAdaptor { 48 | private final Options options; 49 | 50 | public StatementAdaptor(Options options) { 51 | this.options = options; 52 | } 53 | 54 | public void addParameters(PreparedStatement ps, Object[] parameters) throws SQLException { 55 | for (int i = 0; i < parameters.length; i++) { 56 | Object parameter = parameters[i]; 57 | 58 | // Unwrap secret args here so we can use them 59 | if (parameter instanceof SecretArg) { 60 | parameter = ((SecretArg) parameter).getArg(); 61 | } 62 | 63 | if (parameter == null) { 64 | ParameterMetaData metaData; 65 | int parameterType; 66 | try { 67 | metaData = ps.getParameterMetaData(); 68 | parameterType = metaData.getParameterType(i + 1); 69 | } catch (SQLException e) { 70 | throw new DatabaseException("Parameter " + (i + 1) 71 | + " was null and the JDBC driver could not report the type of this column." 72 | + " Please update the JDBC driver to support PreparedStatement.getParameterMetaData()" 73 | + " or use SqlNull in place of null values to this query.", e); 74 | } 75 | ps.setNull(i + 1, parameterType); 76 | } else if (parameter instanceof SqlNull) { 77 | SqlNull sqlNull = (SqlNull) parameter; 78 | if (options.useBytesForBlob() && sqlNull.getType() == Types.BLOB) { 79 | // The setNull() seems more correct, but PostgreSQL chokes on it 80 | ps.setBytes(i + 1, null); 81 | } else { 82 | ps.setNull(i + 1, sqlNull.getType()); 83 | } 84 | } else if (parameter instanceof java.sql.Date) { 85 | ps.setDate( i + 1, (java.sql.Date) parameter); 86 | } else if (parameter instanceof Date) { 87 | // this will correct the millis and nanos according to the JDBC spec 88 | // if a correct Timestamp is passed in, this will detect that and leave it alone 89 | ps.setTimestamp(i + 1, toSqlTimestamp((Date) parameter), options.calendarForTimestamps()); 90 | } else if (parameter instanceof Reader) { 91 | if (options.useStringForClob()) { 92 | ps.setString(i + 1, readerToString((Reader) parameter)); 93 | } else { 94 | ps.setCharacterStream(i + 1, (Reader) parameter); 95 | } 96 | } else if (parameter instanceof InputStream) { 97 | if (options.useBytesForBlob()) { 98 | ps.setBytes(i + 1, streamToBytes((InputStream) parameter)); 99 | } else { 100 | ps.setBinaryStream(i + 1, (InputStream) parameter); 101 | } 102 | } else if (parameter instanceof Float) { 103 | if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) { 104 | // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow 105 | // for small values so we need to use the proprietary mechanism 106 | ps.unwrap(OraclePreparedStatement.class).setBinaryFloat(i + 1, (Float) parameter); 107 | } else { 108 | ps.setFloat(i + 1, (Float) parameter); 109 | } 110 | } else if (parameter instanceof Double) { 111 | if (options.flavor() == Flavor.oracle && ps.isWrapperFor(OraclePreparedStatement.class)) { 112 | // The Oracle 11 driver setDouble() first converts the double to NUMBER, causing underflow 113 | // for small values so we need to use the proprietary mechanism 114 | ps.unwrap(OraclePreparedStatement.class).setBinaryDouble(i + 1, (Double) parameter); 115 | } else { 116 | ps.setDouble(i + 1, (Double) parameter); 117 | } 118 | } else { 119 | ps.setObject(i + 1, parameter); 120 | } 121 | } 122 | } 123 | 124 | private static String readerToString(Reader r) { 125 | Scanner s = new Scanner(r).useDelimiter("\\A"); 126 | return s.hasNext() ? s.next() : ""; 127 | } 128 | 129 | private static byte[] streamToBytes(InputStream is) throws SQLException { 130 | ByteArrayOutputStream out = new ByteArrayOutputStream(); 131 | byte[] buffer = new byte[1024]; 132 | int length; 133 | 134 | try { 135 | while ((length = is.read(buffer)) != -1) { 136 | out.write(buffer, 0, length); 137 | } 138 | } catch (IOException e) { 139 | throw new SQLException("Unable to convert InputStream parameter to bytes", e); 140 | } 141 | 142 | return out.toByteArray(); 143 | } 144 | 145 | /** 146 | * Converts the java.util.Date into a java.sql.Timestamp, following the nanos/millis canonicalization 147 | * required by the spec. If a java.sql.Timestamp is passed in (since it extends java.util.Date), 148 | * it will be checked and canonicalized only if not already correct. 149 | */ 150 | private static Timestamp toSqlTimestamp(Date date) { 151 | long millis = date.getTime(); 152 | int fractionalSecondMillis = (int) (millis % 1000); // guaranteed < 1000 153 | 154 | if (fractionalSecondMillis == 0) { // this means it's already correct by the spec 155 | if (date instanceof Timestamp) { 156 | return (Timestamp) date; 157 | } else { 158 | return new Timestamp(millis); 159 | } 160 | } else { // the millis are invalid and need to be corrected 161 | int tsNanos = fractionalSecondMillis * 1000000; 162 | long tsMillis = millis - fractionalSecondMillis; 163 | Timestamp timestamp = new Timestamp(tsMillis); 164 | timestamp.setNanos(tsNanos); 165 | return timestamp; 166 | } 167 | } 168 | 169 | class SqlNull { 170 | int type; 171 | 172 | SqlNull(int type) { 173 | this.type = type; 174 | } 175 | 176 | int getType() { 177 | return type; 178 | } 179 | } 180 | 181 | public Object nullDate(Date arg) { 182 | if (arg == null) { 183 | return new SqlNull(Types.TIMESTAMP); 184 | } 185 | return new Timestamp(arg.getTime()); 186 | } 187 | 188 | 189 | // Processes a true date without time information. 190 | public Object nullLocalDate(LocalDate arg) { 191 | if (arg == null) { 192 | return new SqlNull(Types.DATE); 193 | } 194 | 195 | return java.sql.Date.valueOf(arg); 196 | } 197 | 198 | public Object nullNumeric(Number arg) { 199 | if (arg == null) { 200 | return new SqlNull(Types.NUMERIC); 201 | } 202 | return arg; 203 | } 204 | 205 | public Object nullString(String arg) { 206 | if (arg == null) { 207 | return new SqlNull(Types.VARCHAR); 208 | } 209 | return arg; 210 | } 211 | 212 | public Object nullClobReader(Reader arg) { 213 | if (arg == null) { 214 | return new SqlNull(Types.VARCHAR); 215 | } 216 | return arg; 217 | } 218 | 219 | public Object nullBytes(byte[] arg) { 220 | if (arg == null) { 221 | return new SqlNull(Types.BLOB); 222 | } 223 | return arg; 224 | } 225 | 226 | public Object nullInputStream(InputStream arg) { 227 | if (arg == null) { 228 | return new SqlNull(Types.BLOB); 229 | } 230 | return arg; 231 | } 232 | 233 | public void closeQuietly(@Nullable ResultSet rs, @Nullable Logger log) { 234 | if (rs != null) { 235 | try { 236 | rs.close(); 237 | } catch (Exception e) { 238 | if (log != null) { 239 | log.warn("Caught exception closing the ResultSet", e); 240 | } 241 | } 242 | } 243 | } 244 | 245 | public void closeQuietly(@Nullable Statement s, @Nullable Logger log) { 246 | if (s != null) { 247 | try { 248 | s.close(); 249 | } catch (Exception e) { 250 | if (log != null) { 251 | log.warn("Caught exception closing the Statement", e); 252 | } 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/Transaction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.susom.database; 17 | 18 | /** 19 | * Allow customization of the transaction behavior. 20 | */ 21 | public interface Transaction { 22 | /** 23 | * @return whether this code block has requested rollback upon a {@link Throwable} 24 | * being thrown from the run method - this only reflects what was requested 25 | * by calling {@link #setRollbackOnError(boolean)}, which is not necessarily 26 | * what will actually happen 27 | */ 28 | boolean isRollbackOnError(); 29 | 30 | /** 31 | * Use this to request either "commit always" or "commit unless error" behavior. 32 | * This will have no effect if {@link #isRollbackOnly()} returns true. 33 | * 34 | * @param rollbackOnError true to rollback after errors; false to commit or rollback based on 35 | * the other settings 36 | * @see DatabaseProvider#transact(DbCodeTx) 37 | */ 38 | void setRollbackOnError(boolean rollbackOnError); 39 | 40 | /** 41 | * @return whether this code block has requested unconditional rollback - this only 42 | * reflects what was requested by calling {@link #setRollbackOnly(boolean)}, 43 | * which is not necessarily what will actually happen 44 | */ 45 | boolean isRollbackOnly(); 46 | 47 | /** 48 | *

If your code inside run() decides for some reason the transaction should rollback 49 | * rather than commit, use this method.

50 | * 51 | * @param rollbackOnly true to request an unconditional rollback; false to commit or rollback based on 52 | * the other settings 53 | * @see DatabaseProvider#transact(DbCodeTx) 54 | */ 55 | void setRollbackOnly(boolean rollbackOnly); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/TransactionImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Simple bean representing how the database transaction should behave 21 | * in terms of commit/rollback. 22 | * 23 | * @author garricko 24 | */ 25 | public class TransactionImpl implements Transaction { 26 | private boolean rollbackOnError; 27 | private boolean rollbackOnly; 28 | 29 | @Override 30 | public boolean isRollbackOnError() { 31 | return rollbackOnError; 32 | } 33 | 34 | @Override 35 | public void setRollbackOnError(boolean rollbackOnError) { 36 | this.rollbackOnError = rollbackOnError; 37 | } 38 | 39 | @Override 40 | public boolean isRollbackOnly() { 41 | return rollbackOnly; 42 | } 43 | 44 | @Override 45 | public void setRollbackOnly(boolean rollbackOnly) { 46 | this.rollbackOnly = rollbackOnly; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/VertxUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database; 2 | 3 | import java.util.Map; 4 | 5 | import org.slf4j.MDC; 6 | 7 | import io.vertx.core.AsyncResult; 8 | import io.vertx.core.Context; 9 | import io.vertx.core.Handler; 10 | import io.vertx.core.Promise; 11 | import io.vertx.core.Vertx; 12 | import io.vertx.core.WorkerExecutor; 13 | 14 | /** 15 | * This is a convenience class to work-around issues with using the SLF4J 16 | * MDC class. It provides versions of async functionality that preserve 17 | * the MDC across the event and worker threads. 18 | * 19 | * @author garricko 20 | */ 21 | public class VertxUtil { 22 | /** 23 | * Wrap a Handler in a way that will preserve the SLF4J MDC context. 24 | * The context from the current thread at the time of this method call 25 | * will be cached and restored within the wrapper at the time the 26 | * handler is invoked. This version delegates the handler call directly 27 | * on the thread that calls it. 28 | */ 29 | public static Handler mdc(final Handler handler) { 30 | if (handler == null) { 31 | // Throw here instead of getting NPE inside the handler so we can see the stack trace 32 | throw new IllegalArgumentException("handler may not be null"); 33 | } 34 | 35 | final Map mdc = MDC.getCopyOfContextMap(); 36 | 37 | return t -> { 38 | Map restore = MDC.getCopyOfContextMap(); 39 | try { 40 | if (mdc == null) { 41 | MDC.clear(); 42 | } else { 43 | MDC.setContextMap(mdc); 44 | } 45 | handler.handle(t); 46 | } finally { 47 | if (restore == null) { 48 | MDC.clear(); 49 | } else { 50 | MDC.setContextMap(restore); 51 | } 52 | } 53 | }; 54 | } 55 | 56 | /** 57 | * Wrap a Handler in a way that will preserve the SLF4J MDC context. 58 | * The context from the current thread at the time of this method call 59 | * will be cached and restored within the wrapper at the time the 60 | * handler is invoked. This version delegates the handler call using 61 | * {@link Context#runOnContext(Handler)} from the current context that 62 | * calls this method, ensuring the handler call will run on the correct 63 | * event loop. 64 | */ 65 | public static Handler mdcEventLoop(final Handler handler) { 66 | if (handler == null) { 67 | // Throw here instead of getting NPE inside the handler so we can see the stack trace 68 | throw new IllegalArgumentException("handler may not be null"); 69 | } 70 | 71 | final Map mdc = MDC.getCopyOfContextMap(); 72 | final Context context = Vertx.currentContext(); 73 | 74 | if (context == null) { 75 | // Throw here instead of getting NPE inside the handler so we can see the stack trace 76 | throw new IllegalStateException("Expecting to be on an Vert.x event loop context"); 77 | } 78 | 79 | return t -> context.runOnContext((v) -> { 80 | Map restore = MDC.getCopyOfContextMap(); 81 | try { 82 | if (mdc == null) { 83 | MDC.clear(); 84 | } else { 85 | MDC.setContextMap(mdc); 86 | } 87 | handler.handle(t); 88 | } finally { 89 | if (restore == null) { 90 | MDC.clear(); 91 | } else { 92 | MDC.setContextMap(restore); 93 | } 94 | } 95 | }); 96 | } 97 | 98 | /** 99 | * Equivalent to {@link Vertx#executeBlocking(Handler, Handler)}, 100 | * but preserves the {@link MDC} correctly. 101 | */ 102 | public static void executeBlocking(Vertx vertx, Handler> promise, Handler> handler) { 103 | executeBlocking(vertx, promise, true, handler); 104 | } 105 | 106 | /** 107 | * Equivalent to {@link Vertx#executeBlocking(Handler, boolean, Handler)}, 108 | * but preserves the {@link MDC} correctly. 109 | */ 110 | public static void executeBlocking(Vertx vertx, Handler> promise, boolean ordered, 111 | Handler> handler) { 112 | vertx.executeBlocking(mdc(promise), ordered, mdcEventLoop(handler)); 113 | } 114 | 115 | /** 116 | * Equivalent to {@link Vertx#executeBlocking(Handler, Handler)}, 117 | * but preserves the {@link MDC} correctly. 118 | */ 119 | public static void executeBlocking(WorkerExecutor executor, Handler> promise, Handler> handler) { 120 | executeBlocking(executor, promise, true, handler); 121 | } 122 | 123 | /** 124 | * Equivalent to {@link Vertx#executeBlocking(Handler, boolean, Handler)}, 125 | * but preserves the {@link MDC} correctly. 126 | */ 127 | public static void executeBlocking(WorkerExecutor executor, Handler> promise, boolean ordered, 128 | Handler> handler) { 129 | executor.executeBlocking(mdc(promise), ordered, mdcEventLoop(handler)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/When.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | import java.util.Objects; 20 | import javax.annotation.Nonnull; 21 | 22 | /** 23 | * Convenience for conditional SQL generation. 24 | */ 25 | public class When { 26 | private String chosen; 27 | private final Flavor actualFlavor; 28 | 29 | public When(Flavor actualFlavor) { 30 | this.actualFlavor = actualFlavor; 31 | } 32 | 33 | @Nonnull 34 | public When oracle(@Nonnull String sql) { 35 | if (actualFlavor == Flavor.oracle) { 36 | chosen = sql; 37 | } 38 | return this; 39 | } 40 | 41 | @Nonnull 42 | public When derby(@Nonnull String sql) { 43 | if (actualFlavor == Flavor.derby) { 44 | chosen = sql; 45 | } 46 | return this; 47 | } 48 | 49 | @Nonnull 50 | public When postgres(@Nonnull String sql) { 51 | if (actualFlavor == Flavor.postgresql) { 52 | chosen = sql; 53 | } 54 | return this; 55 | } 56 | 57 | @Nonnull 58 | public When sqlserver(@Nonnull String sql){ 59 | if (actualFlavor == Flavor.sqlserver) { 60 | chosen = sql; 61 | } 62 | return this; 63 | } 64 | 65 | @Nonnull 66 | public String other(@Nonnull String sql) { 67 | if (chosen == null) { 68 | chosen = sql; 69 | } 70 | return chosen; 71 | } 72 | 73 | @Override 74 | public boolean equals(Object o) { 75 | if (this == o) { 76 | return true; 77 | } 78 | if (o == null || getClass() != o.getClass()) { 79 | return false; 80 | } 81 | When when = (When) o; 82 | return Objects.equals(chosen, when.chosen) && actualFlavor == when.actualFlavor; 83 | } 84 | 85 | @Override 86 | public int hashCode() { 87 | return Objects.hash(chosen, actualFlavor); 88 | } 89 | 90 | @Override 91 | public String toString() { 92 | return chosen == null ? "" : chosen; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/susom/database/WrongNumberOfRowsException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database; 18 | 19 | /** 20 | * Thrown when inserting/updating rows and the actual number of rows modified does 21 | * not match the expected number of rows. 22 | * 23 | * @author garricko 24 | */ 25 | public class WrongNumberOfRowsException extends DatabaseException { 26 | public WrongNumberOfRowsException(String message) { 27 | super(message); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/Database.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.tainting.qual.Untainted; 2 | 3 | package com.github.susom.database; 4 | 5 | class Database { 6 | SqlInsert toInsert(@Untainted String sql); 7 | 8 | SqlSelect toSelect(@Untainted String sql); 9 | 10 | SqlSelect toSelect(@Untainted Sql sql); 11 | 12 | SqlUpdate toUpdate(@Untainted String sql); 13 | 14 | SqlUpdate toDelete(@Untainted String sql); 15 | 16 | Ddl ddl(@Untainted String sql); 17 | 18 | Long nextSequenceValue(@Untainted String sequenceName); 19 | 20 | @Untainted When when(); 21 | 22 | void dropSequenceQuietly(@Untainted String sequenceName); 23 | 24 | void dropTableQuietly(@Untainted String tableName); 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/Sql.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.tainting.qual.Untainted; 2 | 3 | package com.github.susom.database; 4 | 5 | @Untainted 6 | class Sql { 7 | Sql(@Untainted String sql); 8 | 9 | static Sql insert(@Untainted String table, SqlArgs args); 10 | 11 | static Sql insert(@Untainted String table, List args); 12 | 13 | Sql append(@Untainted String sql); 14 | 15 | Sql replace(int start, int end, @Untainted String sql); 16 | 17 | Sql insert(int offset, @Untainted String sql); 18 | 19 | Sql listStart(@Untainted String sql); 20 | 21 | Sql listSeparator(@Untainted String sql); 22 | 23 | Sql listEnd(@Untainted String sql); 24 | 25 | @Untainted 26 | String sql(); 27 | 28 | @Untainted 29 | String toString(); 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/SqlInsert.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.tainting.qual.Untainted; 2 | 3 | package com.github.susom.database; 4 | 5 | class SqlInsert { 6 | Long insertReturningPkSeq(@Untainted String primaryKeyColumnName); 7 | 8 | T insertReturning(@Untainted String tableName, @Untainted String primaryKeyColumnName, RowsHandler rowsHandler, 9 | @Untainted String...otherColumnNames); 10 | 11 | SqlInsert argPkSeq(@Untainted String sequenceName); 12 | 13 | SqlInsert argPkSeq(String argName, @Untainted String sequenceName); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/When.astub: -------------------------------------------------------------------------------- 1 | import org.checkerframework.checker.tainting.qual.Untainted; 2 | 3 | package com.github.susom.database; 4 | 5 | @Untainted 6 | class When { 7 | When oracle(@Untainted String sql); 8 | 9 | When derby(@Untainted String sql); 10 | 11 | When postgres(@Untainted String sql); 12 | 13 | @Untainted 14 | String other(@Untainted String sql); 15 | 16 | @Untainted 17 | String toString(); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/DerbyExample.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import java.io.File; 4 | 5 | import com.github.susom.database.Database; 6 | import com.github.susom.database.DatabaseProvider; 7 | import com.github.susom.database.DatabaseProvider.Builder; 8 | 9 | /** 10 | * Demo of using some com.github.susom.database classes with Derby. 11 | */ 12 | public abstract class DerbyExample { 13 | void example(Database db, String[] args) { 14 | // For subclasses to override 15 | } 16 | 17 | void example(Builder dbb, final String[] args) { 18 | dbb.transact(db -> { 19 | example(db.get(), args); 20 | }); 21 | } 22 | 23 | public void println(String s) { 24 | System.out.println(s); 25 | } 26 | 27 | public final void launch(final String[] args) { 28 | try { 29 | // Put all Derby related files inside ./target to keep our working copy clean 30 | File directory = new File("target").getAbsoluteFile(); 31 | if (directory.exists() || directory.mkdirs()) { 32 | System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath()); 33 | } 34 | 35 | String url = "jdbc:derby:target/testdb;create=true"; 36 | example(DatabaseProvider.fromDriverManager(url), args); 37 | } catch (Exception e) { 38 | e.printStackTrace(); 39 | System.exit(1); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/DynamicSql.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import com.github.susom.database.Database; 4 | import com.github.susom.database.Schema; 5 | import com.github.susom.database.Sql; 6 | import com.github.susom.database.example.DerbyExample; 7 | 8 | /** 9 | * Demo of how to use the Sql helper class to dynamically build queries. 10 | */ 11 | public class DynamicSql extends DerbyExample { 12 | void example(Database db, String[] args) { 13 | // Drops in case we are running this multiple times 14 | db.dropTableQuietly("t"); 15 | 16 | // Create and populate a simple table 17 | new Schema() 18 | .addTable("t") 19 | .addColumn("pk").primaryKey().table() 20 | .addColumn("s").asString(80).schema().execute(db); 21 | db.toInsert("insert into t (pk,s) values (?,?)") 22 | .argLong(1L).argString("Hi").insert(1); 23 | db.toInsert("insert into t (pk,s) values (?,?)") 24 | .argLong(2L).argString("Hi").insert(1); 25 | 26 | // Construct various dynamic queries and execute them 27 | println("Rows with none: " + countByPkOrS(db, null, null)); 28 | println("Rows with pk=1: " + countByPkOrS(db, 1L, null)); 29 | println("Rows with s=Hi: " + countByPkOrS(db, null, "Hi")); 30 | } 31 | 32 | Long countByPkOrS(Database db, Long pk, String s) { 33 | Sql sql = new Sql("select count(*) from t"); 34 | 35 | boolean where = true; 36 | if (pk != null) { 37 | where = false; 38 | sql.append(" where pk=?").argLong(pk); 39 | } 40 | if (s != null) { 41 | if (where) { 42 | sql.append(" where "); 43 | } else { 44 | sql.append(" and "); 45 | } 46 | sql.append("s=?").argString(s); 47 | } 48 | 49 | return db.toSelect(sql).queryLongOrNull(); 50 | } 51 | 52 | public static void main(String[] args) { 53 | new DynamicSql().launch(args); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/FakeBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import com.github.susom.database.DatabaseException; 4 | import com.github.susom.database.DatabaseProvider; 5 | import com.github.susom.database.DatabaseProvider.Builder; 6 | import com.github.susom.database.Schema; 7 | 8 | /** 9 | * Demo of how to use the {@code DatabaseProvider.fakeBuilder()} to control 10 | * transactions for testing purposes. 11 | */ 12 | public class FakeBuilder extends DerbyExample { 13 | void example(Builder dbb, String[] args) { 14 | DatabaseProvider realDbp = null; 15 | 16 | try { 17 | realDbp = dbb.create(); 18 | 19 | dbb.transact(db -> { 20 | // Drops in case we are running this multiple times 21 | db.get().dropTableQuietly("t"); 22 | 23 | // Create and populate a simple table 24 | new Schema().addTable("t").addColumn("pk").primaryKey().schema().execute(db.get()); 25 | }); 26 | 27 | Builder fakeBuilder = realDbp.fakeBuilder(); 28 | 29 | // Trying all three transact methods, just for completeness 30 | fakeBuilder.transact(db -> { 31 | db.get().toInsert("insert into t (pk) values (?)").argLong(1L).insert(1); 32 | }); 33 | fakeBuilder.transact((db, tx) -> { 34 | db.get().toInsert("insert into t (pk) values (?)").argLong(2L).insert(1); 35 | }); 36 | 37 | fakeBuilder.transact(db -> { 38 | println("Rows before rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero()); 39 | }); 40 | 41 | realDbp.rollbackAndClose(); 42 | 43 | // Can't use fakeBuilder after close 44 | try { 45 | fakeBuilder.transact(db -> { 46 | db.get().tableExists("foo"); 47 | println("Eeek...shouldn't get here!"); 48 | }); 49 | } catch(DatabaseException e) { 50 | println("Correctly threw exception: " + e.getMessage()); 51 | } 52 | 53 | dbb.transact(db -> { 54 | println("Rows after rollback: " + db.get().toSelect("select count(*) from t").queryLongOrZero()); 55 | }); 56 | } finally { 57 | if (realDbp != null) { 58 | realDbp.rollbackAndClose(); 59 | } 60 | } 61 | } 62 | 63 | public static void main(String[] args) { 64 | new FakeBuilder().launch(args); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/HelloAny.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import com.github.susom.database.Database; 4 | import com.github.susom.database.DatabaseProvider; 5 | 6 | /** 7 | * Example with database info provided from command line. To use this, set properties like this: 8 | *
9 | *
10 |  *   -Ddatabase.url=...      Database connect string (required)
11 |  *   -Ddatabase.user=...     Authenticate as this user (optional if provided in url)
12 |  *   -Ddatabase.password=... User password (optional if user and password provided in
13 |  *                           url; prompted on standard input if user is provided and
14 |  *                           password is not)
15 |  *   -Ddatabase.flavor=...   What kind of database it is (optional, will guess based
16 |  *                           on the url if this is not provided)
17 |  *   -Ddatabase.driver=...   The Java class of the JDBC driver to load (optional, will
18 |  *                           guess based on the flavor if this is not provided)
19 |  * 
20 | */ 21 | public class HelloAny { 22 | public void run() { 23 | DatabaseProvider.fromSystemProperties().transact(dbp -> { 24 | Database db = dbp.get(); 25 | db.dropTableQuietly("t"); 26 | db.ddl("create table t (a numeric)").execute(); 27 | db.toInsert("insert into t (a) values (?)") 28 | .argInteger(32) 29 | .insert(1); 30 | db.toUpdate("update t set a=:val") 31 | .argInteger("val", 23) 32 | .update(1); 33 | 34 | Long rows = db.toSelect("select count(1) from t ").queryLongOrNull(); 35 | System.out.println("Rows: " + rows); 36 | }); 37 | } 38 | 39 | public static void main(final String[] args) { 40 | try { 41 | new HelloAny().run(); 42 | } catch (Exception e) { 43 | e.printStackTrace(); 44 | System.exit(1); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/HelloDerby.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import java.io.File; 4 | 5 | import com.github.susom.database.Database; 6 | import com.github.susom.database.DatabaseProvider; 7 | 8 | /** 9 | * Demo of using some com.github.susom.database classes with Derby. 10 | */ 11 | public class HelloDerby { 12 | public void run() { 13 | // Put all Derby related files inside ./build to keep our working copy clean 14 | File directory = new File("target").getAbsoluteFile(); 15 | if (directory.exists() || directory.mkdirs()) { 16 | System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath()); 17 | } 18 | 19 | String url = "jdbc:derby:target/testdb;create=true"; 20 | DatabaseProvider.fromDriverManager(url).transact(dbp -> { 21 | Database db = dbp.get(); 22 | db.ddl("drop table t").executeQuietly(); 23 | db.ddl("create table t (a numeric)").execute(); 24 | db.toInsert("insert into t (a) values (?)").argInteger(32).insert(1); 25 | db.toUpdate("update t set a=:val") 26 | .argInteger("val", 23) 27 | .update(1); 28 | 29 | Long rows = db.toSelect("select count(1) from t ").queryLongOrNull(); 30 | println("Rows: " + rows); 31 | }); 32 | } 33 | 34 | public void println(String s) { 35 | System.out.println(s); 36 | } 37 | 38 | public static void main(final String[] args) { 39 | try { 40 | new HelloDerby().run(); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | System.exit(1); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/InsertReturning.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import com.github.susom.database.Database; 4 | import com.github.susom.database.Schema; 5 | 6 | /** 7 | * Demo of using some com.github.susom.database classes with Derby. 8 | */ 9 | public class InsertReturning extends DerbyExample { 10 | void example(Database db, String[] args) { 11 | // Drops in case we are running this multiple times 12 | db.dropTableQuietly("t"); 13 | db.dropSequenceQuietly("pk_seq"); 14 | 15 | // Create a table and a sequence 16 | new Schema() 17 | .addTable("t") 18 | .addColumn("pk").primaryKey().table() 19 | .addColumn("d").asDate().table() 20 | .addColumn("s").asString(80).schema() 21 | .addSequence("pk_seq").schema().execute(db); 22 | 23 | // Insert a row into the table, populating the primary key from a sequence, 24 | // and the date based on current database time. Observe that this will work 25 | // on Derby, where it results in a query for the sequence value, followed by 26 | // the insert. On databases like Oracle, this will be optimized into a single 27 | // statement that does the insert and also returns the primary key. 28 | Long pk = db.toInsert( 29 | "insert into t (pk,d,s) values (?,?,?)") 30 | .argPkSeq("pk_seq") 31 | .argDateNowPerDb() 32 | .argString("Hi") 33 | .insertReturningPkSeq("pk"); 34 | 35 | println("Inserted row with pk=" + pk); 36 | } 37 | 38 | public static void main(String[] args) { 39 | new InsertReturning().launch(args); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/JettyServer.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import org.eclipse.jetty.server.Request; 10 | import org.eclipse.jetty.server.Server; 11 | import org.eclipse.jetty.server.handler.AbstractHandler; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import com.github.susom.database.Config; 16 | import com.github.susom.database.DatabaseProvider; 17 | import com.github.susom.database.DatabaseProvider.Builder; 18 | import com.github.susom.database.Metric; 19 | import com.github.susom.database.Schema; 20 | 21 | /** 22 | * Demo of using some com.github.susom.database classes with Jetty and HyperSQL. 23 | */ 24 | public class JettyServer { 25 | private static final Logger log = LoggerFactory.getLogger(JettyServer.class); 26 | 27 | public void run() throws Exception { 28 | // Set up the database pool 29 | Config config = Config.from().value("database.url", "jdbc:hsqldb:file:target/hsqldb;shutdown=true").get(); 30 | Builder dbb = DatabaseProvider.pooledBuilder(config) 31 | .withSqlInExceptionMessages() 32 | .withSqlParameterLogging(); 33 | 34 | // Set up a table with some data we can query later 35 | dbb.transact(db -> { 36 | db.get().dropTableQuietly("t"); 37 | new Schema().addTable("t") 38 | .addColumn("pk").primaryKey().table() 39 | .addColumn("message").asString(80).schema().execute(db); 40 | 41 | db.get().toInsert("insert into t (pk,message) values (?,?)") 42 | .argInteger(1).argString("Hello World!").batch() 43 | .argInteger(2).argString("Goodbye!").insertBatch(); 44 | }); 45 | 46 | // Start our server 47 | Server server = new Server(8080); 48 | server.setHandler(new AbstractHandler() { 49 | @Override 50 | public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) 51 | throws IOException, ServletException { 52 | Metric metric = new Metric(true); 53 | try { 54 | // Read the query parameter from the request 55 | String pkParam = request.getParameter("pk"); 56 | if (pkParam == null) { 57 | // Probably a favicon or similar request we ignore for now 58 | response.setStatus(404); 59 | baseRequest.setHandled(true); 60 | return; 61 | } 62 | int pk = Integer.parseInt(pkParam); 63 | 64 | // Lookup the message from the database 65 | dbb.transact(db -> { 66 | // Note this part happens on a worker thread 67 | metric.checkpoint("worker"); 68 | String s = db.get().toSelect("select message from t where pk=?").argInteger(pk).queryStringOrEmpty(); 69 | metric.checkpoint("db"); 70 | metric.checkpoint("result"); 71 | metric.done("sent", s.length()); 72 | response.setContentType("text/plain"); 73 | response.setStatus(200); 74 | response.getOutputStream().print(s); 75 | baseRequest.setHandled(true); 76 | }); 77 | } catch (Exception e) { 78 | log.error("Returning 500 to client", e); 79 | response.setStatus(500); 80 | baseRequest.setHandled(true); 81 | } 82 | log.debug("Served request: " + metric.getMessage()); 83 | } 84 | }); 85 | 86 | server.start(); 87 | log.info("Started server. Go to http://localhost:8080/?pk=1 or http://localhost:8080/?pk=2"); 88 | 89 | // Attempt to do a clean shutdown on JVM exit 90 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 91 | log.debug("Trying to stop the server nicely"); 92 | try { 93 | // First shutdown Vert.x 94 | server.stop(); 95 | log.debug("Jetty stopped, now closing the connection pool"); 96 | dbb.close(); 97 | log.debug("Connection pool closed"); 98 | } catch (Exception e) { 99 | e.printStackTrace(); 100 | } 101 | })); 102 | server.join(); 103 | } 104 | 105 | public static void main(final String[] args) { 106 | try { 107 | new JettyServer().run(); 108 | } catch (Exception e) { 109 | e.printStackTrace(); 110 | System.exit(1); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/Sample.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Simple bean for use with SampleDao. 7 | */ 8 | public class Sample { 9 | private Long sampleId; 10 | private String name; 11 | private Integer updateSequence; 12 | private Date updateTime; 13 | 14 | public Long getSampleId() { 15 | return sampleId; 16 | } 17 | 18 | public void setSampleId(Long sampleId) { 19 | this.sampleId = sampleId; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public Integer getUpdateSequence() { 31 | return updateSequence; 32 | } 33 | 34 | public void setUpdateSequence(Integer updateSequence) { 35 | this.updateSequence = updateSequence; 36 | } 37 | 38 | public Date getUpdateTime() { 39 | return updateTime; 40 | } 41 | 42 | public void setUpdateTime(Date updateTime) { 43 | this.updateTime = updateTime; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/SampleDao.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import java.util.Date; 4 | import java.util.function.Supplier; 5 | 6 | import com.github.susom.database.Database; 7 | 8 | /** 9 | * Create, read, update, and delete sample database objects. 10 | */ 11 | public class SampleDao { 12 | private final Supplier dbp; 13 | 14 | public SampleDao(Supplier dbp) { 15 | this.dbp = dbp; 16 | } 17 | 18 | public void createSample(final Sample sample, Long userIdMakingChange) { 19 | Database db = dbp.get(); 20 | 21 | Date updateTime = db.nowPerApp(); 22 | Long sampleId = db.toInsert( 23 | "insert into sample (sample_id, sample_name, update_sequence, update_time) values (?,?,0,?)") 24 | .argPkSeq("id_seq") 25 | .argString(sample.getName()) 26 | .argDate(updateTime) 27 | .insertReturningPkSeq("sample_id"); 28 | 29 | db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," 30 | + " is_deleted) values (?,?,0,?,?,'N')") 31 | .argLong(sampleId) 32 | .argString(sample.getName()) 33 | .argDate(updateTime) 34 | .argLong(userIdMakingChange) 35 | .insert(1); 36 | 37 | // Update the object in memory 38 | sample.setSampleId(sampleId); 39 | sample.setUpdateSequence(0); 40 | sample.setUpdateTime(updateTime); 41 | } 42 | 43 | public Sample findSampleById(final Long sampleId, boolean lockRow) { 44 | return dbp.get().toSelect("select sample_name, update_sequence, update_time from sample where sample_id=?" 45 | + (lockRow ? " for update" : "")) 46 | .argLong(sampleId).queryOneOrNull(r -> { 47 | Sample result = new Sample(); 48 | result.setSampleId(sampleId); 49 | result.setName(r.getStringOrNull()); 50 | result.setUpdateSequence(r.getIntegerOrNull()); 51 | result.setUpdateTime(r.getDateOrNull()); 52 | return result; 53 | }); 54 | } 55 | 56 | public void updateSample(Sample sample, Long userIdMakingChange) { 57 | Database db = dbp.get(); 58 | 59 | // Insert the history row first, so it will fail (non-unique sample_id + update_sequence) 60 | // if someone else modified the row. This is an optimistic locking strategy. 61 | int newUpdateSequence = sample.getUpdateSequence() + 1; 62 | Date newUpdateTime = db.nowPerApp(); 63 | db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," 64 | + " is_deleted) values (?,?,?,?,?,'N')") 65 | .argLong(sample.getSampleId()) 66 | .argString(sample.getName()) 67 | .argInteger(newUpdateSequence) 68 | .argDate(newUpdateTime) 69 | .argLong(userIdMakingChange) 70 | .insert(1); 71 | 72 | db.toUpdate("update sample set sample_name=?, update_sequence=?, update_time=? where sample_id=?") 73 | .argString(sample.getName()) 74 | .argInteger(newUpdateSequence) 75 | .argDate(newUpdateTime) 76 | .argLong(sample.getSampleId()) 77 | .update(1); 78 | 79 | // Make sure the object in memory matches the database. 80 | sample.setUpdateSequence(newUpdateSequence); 81 | sample.setUpdateTime(newUpdateTime); 82 | } 83 | 84 | public void deleteSample(Sample sample, Long userIdMakingChange) { 85 | Database db = dbp.get(); 86 | 87 | // Insert the history row first, so it will fail (non-unique sample_id + update_sequence) 88 | // if someone else modified the row. This is an optimistic locking strategy. 89 | int newUpdateSequence = sample.getUpdateSequence() + 1; 90 | Date newUpdateTime = db.nowPerApp(); 91 | db.toInsert("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id," 92 | + " is_deleted) values (?,?,?,?,?,'Y')") 93 | .argLong(sample.getSampleId()) 94 | .argString(sample.getName()) 95 | .argInteger(newUpdateSequence) 96 | .argDate(newUpdateTime) 97 | .argLong(userIdMakingChange) 98 | .insert(1); 99 | 100 | db.toDelete("delete from sample where sample_id=?") 101 | .argLong(sample.getSampleId()) 102 | .update(1); 103 | 104 | // Make sure the object in memory matches the database. 105 | sample.setUpdateSequence(newUpdateSequence); 106 | sample.setUpdateTime(newUpdateTime); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/VertxServer.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import com.github.susom.database.Config; 7 | import com.github.susom.database.ConfigFrom; 8 | import com.github.susom.database.DatabaseProviderVertx; 9 | import com.github.susom.database.DatabaseProviderVertx.Builder; 10 | import com.github.susom.database.Metric; 11 | import com.github.susom.database.Schema; 12 | 13 | import io.vertx.core.Vertx; 14 | import io.vertx.core.json.JsonObject; 15 | 16 | /** 17 | * Demo of using some com.github.susom.database classes with Vertx and HyperSQL. 18 | */ 19 | public class VertxServer { 20 | private static final Logger log = LoggerFactory.getLogger(VertxServer.class); 21 | private final Object lock = new Object(); 22 | 23 | public void run() throws Exception { 24 | // A JSON config you might get from Vertx. In a real scenario you would 25 | // also set database.user, database.password and database.pool.size. 26 | JsonObject jsonConfig = new JsonObject() 27 | .put("database.url", "jdbc:hsqldb:file:target/hsqldb;shutdown=true"); 28 | 29 | // Set up Vertx and database access 30 | Vertx vertx = Vertx.vertx(); 31 | Config config = ConfigFrom.firstOf().custom(jsonConfig::getString).get(); 32 | Builder dbb = DatabaseProviderVertx.pooledBuilder(vertx, config) 33 | .withSqlInExceptionMessages() 34 | .withSqlParameterLogging(); 35 | 36 | // Set up a table with some data we can query later 37 | dbb.transact(db -> { 38 | db.get().dropTableQuietly("t"); 39 | new Schema().addTable("t") 40 | .addColumn("pk").primaryKey().table() 41 | .addColumn("message").asString(80).schema().execute(db); 42 | 43 | db.get().toInsert("insert into t (pk,message) values (?,?)") 44 | .argInteger(1).argString("Hello World!").batch() 45 | .argInteger(2).argString("Goodbye!").insertBatch(); 46 | }); 47 | 48 | // Start our server 49 | vertx.createHttpServer().requestHandler(request -> { 50 | Metric metric = new Metric(true); 51 | 52 | // Read the query parameter from the request 53 | String pkParam = request.getParam("pk"); 54 | if (pkParam == null) { 55 | // Probably a favicon or similar request we ignore for now 56 | request.response().setStatusCode(404).end(); 57 | return; 58 | } 59 | int pk = Integer.parseInt(pkParam); 60 | 61 | // Lookup the message from the database 62 | dbb.transactAsync(db -> { 63 | // Note this part happens on a worker thread 64 | metric.checkpoint("worker"); 65 | String s = db.get().toSelect("select message from t where pk=?").argInteger(pk).queryStringOrEmpty(); 66 | metric.checkpoint("db"); 67 | return s; 68 | }, result -> { 69 | // Now we are back on the event loop thread 70 | metric.checkpoint("result"); 71 | request.response().bodyEndHandler(h -> { 72 | metric.done("sent", request.response().bytesWritten()); 73 | log.debug("Served request: " + metric.getMessage()); 74 | }); 75 | if (result.succeeded()) { 76 | request.response().setStatusCode(200).putHeader("content-type", "text/plain").end(result.result()); 77 | } else { 78 | log.error("Returning 500 to client", result.cause()); 79 | request.response().setStatusCode(500).end(); 80 | } 81 | }); 82 | }).listen(8123, result -> 83 | log.info("Started server. Go to http://localhost:8123/?pk=1 or http://localhost:8123/?pk=2") 84 | ); 85 | 86 | // Attempt to do a clean shutdown on JVM exit 87 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 88 | log.debug("Trying to stop the server nicely"); 89 | try { 90 | synchronized (lock) { 91 | // First shutdown Vert.x 92 | vertx.close(h -> { 93 | log.debug("Vert.x stopped, now closing the connection pool"); 94 | synchronized (lock) { 95 | // Then shutdown the database pool 96 | dbb.close(); 97 | log.debug("Server stopped"); 98 | lock.notify(); 99 | } 100 | }); 101 | lock.wait(30000); 102 | } 103 | } catch (Exception e) { 104 | e.printStackTrace(); 105 | } 106 | })); 107 | } 108 | 109 | public static void main(final String[] args) { 110 | try { 111 | new VertxServer().run(); 112 | } catch (Exception e) { 113 | e.printStackTrace(); 114 | System.exit(1); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/example/VertxServerFastAndSlow.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.example; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import com.github.susom.database.Config; 7 | import com.github.susom.database.ConfigFrom; 8 | import com.github.susom.database.DatabaseProviderVertx; 9 | import com.github.susom.database.DatabaseProviderVertx.Builder; 10 | import com.github.susom.database.Metric; 11 | import com.github.susom.database.Schema; 12 | 13 | import io.vertx.core.AsyncResult; 14 | import io.vertx.core.Future; 15 | import io.vertx.core.Handler; 16 | import io.vertx.core.Vertx; 17 | 18 | /** 19 | *

Demo of using some com.github.susom.database classes with Vertx and HyperSQL. 20 | * In this version there are two database pools, one for fast queries and one 21 | * for slow queries. The idea is that saturating the slow pool won't affect 22 | * server calls using the fast pool, and saturating both of the pools won't 23 | * affect server calls that do not use the database.

24 | * 25 | *

To test this, you can fire up the server and then run commands like the 26 | * following in parallel in three separate terminals:

27 | * 28 | * 29 | * ab -k -c 50 -n 100 http://localhost:8123/slow 30 | * ab -k -c 500 -n 10000 http://localhost:8123/fast 31 | * ab -k -c 500 -n 10000 http://localhost:8123/static 32 | * 33 | */ 34 | public class VertxServerFastAndSlow { 35 | private static final Logger log = LoggerFactory.getLogger(VertxServerFastAndSlow.class); 36 | private final Object lock = new Object(); 37 | 38 | public void run() throws Exception { 39 | Config config = ConfigFrom.firstOf().value("database.url", "jdbc:hsqldb:file:target/hsqldb;shutdown=true").get(); 40 | Vertx vertx = Vertx.vertx(); 41 | Builder fastDb = DatabaseProviderVertx.pooledBuilder(vertx, config); 42 | Builder slowDb = DatabaseProviderVertx.pooledBuilder(vertx, config); 43 | 44 | // Set up a table with some data we can query later 45 | fastDb.transact(db -> { 46 | db.get().dropTableQuietly("t"); 47 | new Schema().addTable("t") 48 | .addColumn("pk").primaryKey().table() 49 | .addColumn("message").asString(80).schema().execute(db); 50 | 51 | db.get().toInsert("insert into t (pk,message) values (?,?)") 52 | .argInteger(1).argString("Hello World!").batch() 53 | .argInteger(2).argString("Goodbye!").insertBatch(); 54 | }); 55 | 56 | // Start our server 57 | vertx.createHttpServer().requestHandler(request -> { 58 | Metric metric = new Metric(true); 59 | Handler> sendResponse = result -> { 60 | // Now we are back on the event loop thread 61 | metric.checkpoint("result"); 62 | request.response().bodyEndHandler(h -> { 63 | metric.done("sent", request.response().bytesWritten()); 64 | log.debug("Served request: " + metric.getMessage()); 65 | }); 66 | if (result.succeeded()) { 67 | request.response().setStatusCode(200).putHeader("content-type", "text/plain").end(result.result()); 68 | } else { 69 | log.error("Returning 500 to client", result.cause()); 70 | request.response().setStatusCode(500).end(); 71 | } 72 | }; 73 | 74 | switch (request.path()) { 75 | case "/fast": 76 | fastDb.transactAsync(db -> { 77 | metric.checkpoint("worker"); 78 | String s = db.get().toSelect("select message from t where pk=?").argInteger(1).queryStringOrEmpty(); 79 | metric.checkpoint("db"); 80 | return s; 81 | }, sendResponse); 82 | break; 83 | case "/slow": 84 | slowDb.transactAsync(db -> { 85 | metric.checkpoint("worker"); 86 | String s = db.get().toSelect("select message from t where pk=?").argInteger(2).queryStringOrEmpty(); 87 | metric.checkpoint("db"); 88 | // Simulate slow query 89 | Thread.sleep(2000); 90 | metric.checkpoint("sleep"); 91 | return s; 92 | }, sendResponse); 93 | break; 94 | default: 95 | sendResponse.handle(Future.succeededFuture("Hi")); 96 | } 97 | }).listen(8123, result -> 98 | log.info("Started server. Go to one of:\n http://localhost:8123/slow\n http://localhost:8123/fast" 99 | + "\n http://localhost:8123/static") 100 | ); 101 | 102 | // Attempt to do a clean shutdown on JVM exit 103 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 104 | log.debug("Trying to stop the server nicely"); 105 | try { 106 | synchronized (lock) { 107 | // First shutdown Vert.x 108 | vertx.close(h -> { 109 | log.debug("Vert.x stopped, now closing the connection pool"); 110 | synchronized (lock) { 111 | // Then shutdown the database pools 112 | fastDb.close(); 113 | slowDb.close(); 114 | log.debug("Server stopped"); 115 | lock.notify(); 116 | } 117 | }); 118 | lock.wait(30000); 119 | } 120 | } catch (Exception e) { 121 | e.printStackTrace(); 122 | } 123 | })); 124 | } 125 | 126 | public static void main(final String[] args) { 127 | try { 128 | new VertxServerFastAndSlow().run(); 129 | } catch (Exception e) { 130 | e.printStackTrace(); 131 | System.exit(1); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/DemoTest.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import com.github.susom.database.example.DynamicSql; 4 | import com.github.susom.database.example.FakeBuilder; 5 | import com.github.susom.database.example.HelloDerby; 6 | import com.github.susom.database.example.InsertReturning; 7 | import org.junit.Test; 8 | 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.verifyNoMoreInteractions; 12 | 13 | /** 14 | * Make sure each demo class runs and outputs the correct values. 15 | */ 16 | public class DemoTest { 17 | @Test 18 | public void dynamicSql() { 19 | final Output output = mock(Output.class); 20 | 21 | new DynamicSql() { 22 | @Override 23 | public void println(String s) { 24 | output.println(s); 25 | } 26 | }.launch(new String[0]); 27 | 28 | verify(output).println("Rows with none: 2"); 29 | verify(output).println("Rows with pk=1: 1"); 30 | verify(output).println("Rows with s=Hi: 2"); 31 | verifyNoMoreInteractions(output); 32 | } 33 | 34 | @Test 35 | public void helloDerby() { 36 | final Output output = mock(Output.class); 37 | 38 | new HelloDerby() { 39 | @Override 40 | public void println(String s) { 41 | output.println(s); 42 | } 43 | }.run(); 44 | 45 | verify(output).println("Rows: 1"); 46 | verifyNoMoreInteractions(output); 47 | } 48 | 49 | @Test 50 | public void fakeBuilder() { 51 | final Output output = mock(Output.class); 52 | 53 | new FakeBuilder() { 54 | @Override 55 | public void println(String s) { 56 | output.println(s); 57 | } 58 | }.launch(new String[0]); 59 | 60 | verify(output).println("Rows before rollback: 2"); 61 | verify(output).println("Correctly threw exception: Called get() on a DatabaseProvider after close()"); 62 | verify(output).println("Rows after rollback: 0"); 63 | verifyNoMoreInteractions(output); 64 | } 65 | 66 | @Test 67 | public void insertReturning() { 68 | final Output output = mock(Output.class); 69 | 70 | new InsertReturning() { 71 | @Override 72 | public void println(String s) { 73 | output.println(s); 74 | } 75 | }.launch(new String[0]); 76 | 77 | verify(output).println("Inserted row with pk=1"); 78 | verifyNoMoreInteractions(output); 79 | } 80 | 81 | interface Output { 82 | void println(String s); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/DerbyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import java.io.File; 20 | import java.math.BigDecimal; 21 | 22 | import org.junit.Ignore; 23 | import org.junit.Test; 24 | 25 | import com.github.susom.database.DatabaseProvider; 26 | import com.github.susom.database.OptionsOverride; 27 | import com.github.susom.database.Schema; 28 | 29 | import static org.junit.Assert.assertEquals; 30 | 31 | /** 32 | * Exercise Database functionality with a real database (Derby). 33 | * 34 | * @author garricko 35 | */ 36 | public class DerbyTest extends CommonTest { 37 | static { 38 | // We will put all Derby related files inside ./build to keep our working copy clean 39 | File directory = new File("target").getAbsoluteFile(); 40 | if (directory.exists() || directory.mkdirs()) { 41 | System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath()); 42 | } 43 | } 44 | 45 | @Override 46 | protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { 47 | return DatabaseProvider.fromDriverManager("jdbc:derby:target/testdb;create=true") 48 | .withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create(); 49 | } 50 | 51 | // TODO fix this test 52 | @Ignore("Not sure why this fails on the build servers right now...") 53 | @Test 54 | public void clockSync() { 55 | super.clockSync(); 56 | } 57 | 58 | @Ignore("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") 59 | @Test 60 | public void argFloatNaN() { 61 | super.argFloatNaN(); 62 | } 63 | 64 | @Ignore("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") 65 | @Test 66 | public void argFloatInfinity() { 67 | super.argFloatInfinity(); 68 | } 69 | 70 | @Ignore("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") 71 | @Test 72 | public void argDoubleNaN() { 73 | super.argDoubleNaN(); 74 | } 75 | 76 | @Ignore("Derby prohibits NaN and Infinity (https://issues.apache.org/jira/browse/DERBY-3290)") 77 | @Test 78 | public void argDoubleInfinity() { 79 | super.argDoubleInfinity(); 80 | } 81 | 82 | @Ignore("Current Derby behavior is to convert -0f to 0f") 83 | @Test 84 | public void argFloatNegativeZero() { 85 | super.argFloatNegativeZero(); 86 | } 87 | 88 | @Ignore("Current Derby behavior is to convert -0d to 0d") 89 | @Test 90 | public void argDoubleNegativeZero() { 91 | super.argDoubleNegativeZero(); 92 | } 93 | 94 | @Ignore("Derby does not support timestamp intervals") 95 | @Test 96 | public void intervals() { 97 | super.intervals(); 98 | } 99 | 100 | @Test 101 | public void argBigDecimal31Precision0() { 102 | db.dropTableQuietly("dbtest"); 103 | 104 | new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 0).schema().execute(db); 105 | 106 | BigDecimal value = new BigDecimal("9999999999999999999999999999999"); // 31 digits 107 | db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); 108 | assertEquals(value, 109 | db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); 110 | } 111 | 112 | @Test 113 | public void argBigDecimal31Precision1() { 114 | db.dropTableQuietly("dbtest"); 115 | 116 | new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 1).schema().execute(db); 117 | 118 | BigDecimal value = new BigDecimal("999999999999999999999999999999.9"); // 31 digits 119 | db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); 120 | assertEquals(value, 121 | db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); 122 | } 123 | 124 | @Test 125 | public void argBigDecimal31Precision30() { 126 | db.dropTableQuietly("dbtest"); 127 | 128 | new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 30).schema().execute(db); 129 | 130 | BigDecimal value = new BigDecimal("9.999999999999999999999999999999"); // 31 digits 131 | db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); 132 | assertEquals(value, 133 | db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); 134 | } 135 | 136 | @Test 137 | public void argBigDecimal31Precision31() { 138 | db.dropTableQuietly("dbtest"); 139 | 140 | new Schema().addTable("dbtest").addColumn("i").asBigDecimal(31, 31).schema().execute(db); 141 | 142 | BigDecimal value = new BigDecimal("0.9999999999999999999999999999999"); // 31 digits 143 | db.toInsert("insert into dbtest (i) values (?)").argBigDecimal(value).insert(1); 144 | System.out.println(db.toSelect("select i from dbtest").queryBigDecimalOrNull()); 145 | assertEquals(value, 146 | db.toSelect("select i from dbtest where i=?").argBigDecimal(value).queryBigDecimalOrNull()); 147 | } 148 | 149 | @Ignore("Derby limits out at precision 31") 150 | @Test 151 | public void argBigDecimal38Precision0() { 152 | super.argBigDecimal38Precision0(); 153 | } 154 | 155 | @Ignore("Derby limits out at precision 31") 156 | @Test 157 | public void argBigDecimal38Precision1() { 158 | super.argBigDecimal38Precision1(); 159 | } 160 | 161 | @Ignore("Derby limits out at precision 31") 162 | @Test 163 | public void argBigDecimal38Precision37() { 164 | super.argBigDecimal38Precision37(); 165 | } 166 | 167 | @Ignore("Derby limits out at precision 31") 168 | @Test 169 | public void argBigDecimal38Precision38() { 170 | super.argBigDecimal38Precision38(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/OracleTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import java.io.FileReader; 20 | import java.util.Properties; 21 | 22 | import org.junit.Ignore; 23 | import org.junit.Test; 24 | 25 | import com.github.susom.database.DatabaseProvider; 26 | import com.github.susom.database.OptionsOverride; 27 | 28 | /** 29 | * Exercise Database functionality with a real Oracle database. 30 | * 31 | * @author garricko 32 | */ 33 | public class OracleTest extends CommonTest { 34 | @Override 35 | protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { 36 | Properties properties = new Properties(); 37 | try { 38 | properties.load(new FileReader(System.getProperty("local.properties", "local.properties"))); 39 | } catch (Exception e) { 40 | // Don't care, fallback to system properties 41 | } 42 | 43 | return DatabaseProvider.fromDriverManager( 44 | System.getProperty("database.url", properties.getProperty("database.url")), 45 | System.getProperty("database.user", properties.getProperty("database.user")), 46 | System.getProperty("database.password", properties.getProperty("database.password")) 47 | ).withSqlParameterLogging().withSqlInExceptionMessages().withOptions(options).create(); 48 | } 49 | 50 | @Ignore("Current Oracle behavior is to convert -0f to 0f") 51 | @Test 52 | public void argFloatNegativeZero() { 53 | super.argFloatNegativeZero(); 54 | } 55 | 56 | @Ignore("Current Oracle behavior is to convert -0d to 0d") 57 | @Test 58 | public void argDoubleNegativeZero() { 59 | super.argDoubleNegativeZero(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/PostgreSqlTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import java.io.FileReader; 20 | import java.util.Properties; 21 | 22 | import org.junit.Test; 23 | 24 | import com.github.susom.database.DatabaseProvider; 25 | import com.github.susom.database.OptionsOverride; 26 | import com.github.susom.database.Rows; 27 | import com.github.susom.database.RowsHandler; 28 | import com.github.susom.database.Schema; 29 | 30 | import static org.junit.Assert.assertArrayEquals; 31 | 32 | /** 33 | * Exercise Database functionality with a real PostgreSQL database. 34 | * 35 | * @author garricko 36 | */ 37 | public class PostgreSqlTest extends CommonTest { 38 | @Override 39 | protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { 40 | Properties properties = new Properties(); 41 | try { 42 | properties.load(new FileReader(System.getProperty("local.properties", "local.properties"))); 43 | } catch (Exception e) { 44 | // Don't care, fallback to system properties 45 | } 46 | 47 | Class.forName("org.postgresql.Driver"); 48 | 49 | return DatabaseProvider.fromDriverManager( 50 | System.getProperty("postgres.database.url", properties.getProperty("postgres.database.url")), 51 | System.getProperty("postgres.database.user", properties.getProperty("postgres.database.user")), 52 | System.getProperty("postgres.database.password", properties.getProperty("postgres.database.password")) 53 | ).withOptions(options).withSqlParameterLogging().withSqlInExceptionMessages().create(); 54 | } 55 | 56 | /** 57 | * PostgreSQL seems to have different behavior in that is does not convert 58 | * column names to uppercase (it actually converts them to lowercase). 59 | * I haven't figured out how to smooth over this difference, since all databases 60 | * seem to respect the provided case when it is inside quotes, but don't provide 61 | * a way to tell whether a particular parameter was quoted. 62 | */ 63 | @Override 64 | @Test 65 | public void metadataColumnNames() { 66 | db.dropTableQuietly("dbtest"); 67 | 68 | new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); 69 | 70 | db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest") 71 | .query(new RowsHandler() { 72 | @Override 73 | public Object process(Rows rs) throws Exception { 74 | assertArrayEquals(new String[] { "pk", "foo", "Foo" }, rs.getColumnLabels()); 75 | return null; 76 | } 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/Retry.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import java.lang.annotation.ElementType; 20 | import java.lang.annotation.Retention; 21 | import java.lang.annotation.RetentionPolicy; 22 | import java.lang.annotation.Target; 23 | 24 | /** 25 | * Annotation to mark test methods as being non-deterministic (may need a retry). 26 | * See {@link com.github.susom.database.test.Retryable}. 27 | * 28 | * @author garricko 29 | */ 30 | @Retention(RetentionPolicy.RUNTIME) 31 | @Target(ElementType.METHOD) 32 | public @interface Retry { 33 | int maxTries() default 3; 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/Retryable.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import org.junit.rules.TestRule; 20 | import org.junit.runner.Description; 21 | import org.junit.runners.model.Statement; 22 | 23 | /** 24 | * Allow a non-deterministic test to fail up to a certain number of times 25 | * and still pass. 26 | * 27 | * @author garricko 28 | */ 29 | public class Retryable implements TestRule { 30 | public Statement apply(Statement base, Description description) { 31 | return statement(base, description); 32 | } 33 | 34 | private Statement statement(final Statement base, final Description description) { 35 | return new Statement() { 36 | @Override 37 | public void evaluate() throws Throwable { 38 | Retry retry = description.getAnnotation(Retry.class); 39 | 40 | if (retry == null || retry.maxTries() < 2) { 41 | base.evaluate(); 42 | return; 43 | } 44 | 45 | Throwable error = null; 46 | for (int i = 0; i < retry.maxTries(); i++) { 47 | if (error != null) { 48 | error.printStackTrace(); 49 | } 50 | try { 51 | base.evaluate(); 52 | return; 53 | } catch (Throwable t) { 54 | error = t; 55 | } 56 | } 57 | throw error; 58 | } 59 | }; 60 | } 61 | } -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/RowStubMockDao.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import com.github.susom.database.Database; 4 | 5 | import java.time.LocalDate; 6 | import java.time.ZoneId; 7 | import java.util.Date; 8 | import java.util.function.Supplier; 9 | 10 | /** 11 | * Test Create, read, update, and delete using the RowStub implementation. 12 | */ 13 | public class RowStubMockDao { 14 | private final Supplier dbp; 15 | 16 | public RowStubMockDao(Supplier dbp) { 17 | this.dbp = dbp; 18 | } 19 | 20 | public void create(final RowStubMockData data, Long userIdMakingChange) { 21 | Database db = dbp.get(); 22 | 23 | Date updateTime = db.nowPerApp(); 24 | Long dataId = db.toInsert( 25 | "insert into dbtest (data_id, name, local_date, update_sequence, update_time) values (?,?,?,0,?)") 26 | .argPkSeq("id_seq") 27 | .argString(data.getName()) 28 | .argLocalDate(data.getLocalDate()) 29 | .argDate(updateTime) 30 | .insertReturningPkSeq("data_id"); 31 | 32 | // Update the object in memory 33 | data.setDataId(dataId); 34 | data.setUpdateSequence(0); 35 | data.setUpdateTime(updateTime); 36 | } 37 | 38 | public RowStubMockData findById(final Long dataId, ColumnLookupType lookupType, boolean lockRow) throws Exception { 39 | return dbp.get().toSelect("select name, local_date, update_sequence, update_time from dbtest where data_id=?" 40 | + (lockRow ? " for update" : "")) 41 | .argLong(dataId).queryOneOrNull(rowStub -> { 42 | RowStubMockData result = new RowStubMockData(); 43 | result.setDataId(dataId); 44 | switch (lookupType) { 45 | case BY_ORDER: 46 | // Hit the column number path getting the results 47 | result.setName(rowStub.getStringOrNull()); 48 | result.setLocalDate(rowStub.getLocalDateOrNull()); 49 | result.setUpdateSequence(rowStub.getIntegerOrNull()); 50 | result.setUpdateTime(rowStub.getDateOrNull()); 51 | break; 52 | 53 | case BY_NAME: 54 | // Hig the column name path getting the results 55 | result.setName(rowStub.getStringOrNull("name")); 56 | result.setLocalDate(rowStub.getLocalDateOrNull("local_date")); 57 | result.setUpdateSequence(rowStub.getIntegerOrNull("update_sequence")); 58 | result.setUpdateTime(rowStub.getDateOrNull("update_time")); 59 | break; 60 | 61 | case BY_NUMBER: 62 | // Hit the column number path getting the results 63 | result.setName(rowStub.getStringOrNull(1)); 64 | result.setLocalDate(rowStub.getLocalDateOrNull(2)); 65 | result.setUpdateSequence(rowStub.getIntegerOrNull(3)); 66 | result.setUpdateTime(rowStub.getDateOrNull(4)); 67 | break; 68 | default: 69 | throw new Exception("Unexpected Lookup Type in findById!"); 70 | } 71 | return result; 72 | }); 73 | } 74 | 75 | public void update(RowStubMockData data, Long userIdMakingChange) { 76 | Database db = dbp.get(); 77 | 78 | int newUpdateSequence = data.getUpdateSequence() + 1; 79 | Date newUpdateTime = db.nowPerApp(); 80 | LocalDate newLocalDate = newUpdateTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); 81 | 82 | db.toUpdate("update dbtest set name=?, local_date=?, update_sequence=?, update_time=? where data_id=?") 83 | .argString(data.getName()) 84 | .argInteger(newUpdateSequence) 85 | .argLocalDate(newLocalDate) 86 | .argDate(newUpdateTime) 87 | .argLong(data.getDataId()) 88 | .update(1); 89 | 90 | // Make sure the object in memory matches the database. 91 | data.setLocalDate(newLocalDate); 92 | data.setUpdateSequence(newUpdateSequence); 93 | data.setUpdateTime(newUpdateTime); 94 | } 95 | 96 | public void delete(RowStubMockData data, Long userIdMakingChange) { 97 | Database db = dbp.get(); 98 | 99 | int newUpdateSequence = data.getUpdateSequence() + 1; 100 | Date newUpdateTime = db.nowPerApp(); 101 | 102 | db.toDelete("delete from dbtest where data_id=?") 103 | .argLong(data.getDataId()) 104 | .update(1); 105 | 106 | // Make sure the object in memory matches the database. 107 | data.setUpdateSequence(newUpdateSequence); 108 | data.setUpdateTime(newUpdateTime); 109 | } 110 | 111 | public enum ColumnLookupType {BY_ORDER, BY_NAME, BY_NUMBER} 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/RowStubMockData.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import java.time.LocalDate; 4 | import java.util.Date; 5 | 6 | /** 7 | * Simple bean for use by RowStubMockDao mock testing the RowStub implementation 8 | */ 9 | public class RowStubMockData { 10 | private Long dataId; 11 | private String name; 12 | private LocalDate localDate; 13 | private Integer updateSequence; 14 | private Date updateTime; 15 | 16 | public Long getDataId() { 17 | return dataId; 18 | } 19 | 20 | public void setDataId(Long dataId) { 21 | this.dataId = dataId; 22 | } 23 | 24 | public String getName() { 25 | return name; 26 | } 27 | 28 | public void setName(String name) { 29 | this.name = name; 30 | } 31 | 32 | public LocalDate getLocalDate() { 33 | return localDate; 34 | } 35 | 36 | public void setLocalDate(LocalDate localDate) { 37 | this.localDate = localDate; 38 | } 39 | 40 | public Integer getUpdateSequence() { 41 | return updateSequence; 42 | } 43 | 44 | public void setUpdateSequence(Integer updateSequence) { 45 | this.updateSequence = updateSequence; 46 | } 47 | 48 | public Date getUpdateTime() { 49 | return updateTime; 50 | } 51 | 52 | public void setUpdateTime(Date updateTime) { 53 | this.updateTime = updateTime; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/RowStubTest.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import com.github.susom.database.*; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | 10 | import java.time.LocalDate; 11 | import java.util.Date; 12 | 13 | import static org.junit.Assert.*; 14 | import static org.mockito.Mockito.*; 15 | 16 | /** 17 | * Unit tests for exercising the RowStub implementation 18 | */ 19 | public class RowStubTest { 20 | @Mock 21 | private DatabaseMock db; 22 | private Date now = new Date(); 23 | private LocalDate localDateNow = LocalDate.now(); 24 | private RowStubMockDao rowStubMockDao; 25 | 26 | @Before 27 | public void initializeMocks() { 28 | MockitoAnnotations.initMocks(this); 29 | 30 | // This is the key for database mocking. Explicitly create our DatabaseImpl and give it our mock. 31 | // Could use any Flavor here, though postgres/oracle are a little easier than derby because 32 | // they create single operations for insert returning. 33 | rowStubMockDao = new RowStubMockDao(new DatabaseImpl(db, new OptionsOverride(Flavor.postgresql) { 34 | @Override 35 | public Date currentDate() { 36 | // Use a local variable from the test so we can verify in a deterministic way 37 | return now; 38 | } 39 | })); 40 | } 41 | 42 | @Test 43 | public void testCreate() throws Exception { 44 | // Configure the mock because DAO expects the pk to be returned from the insert 45 | when(db.insertReturningPk(anyString(), anyString())).thenReturn(1L); 46 | 47 | RowStubMockData data = new RowStubMockData(); 48 | data.setName("Foo"); 49 | data.setLocalDate(localDateNow); 50 | 51 | rowStubMockDao.create(data, 1L); 52 | 53 | // Verify object in memory is updated properly 54 | assertEquals(Long.valueOf(1L), data.getDataId()); 55 | assertEquals("Foo", data.getName()); 56 | assertEquals(localDateNow, data.getLocalDate()); 57 | assertEquals(Integer.valueOf(0), data.getUpdateSequence()); 58 | assertEquals(now, data.getUpdateTime()); 59 | 60 | // Verify SQL executed against golden copies 61 | verify(db).insertReturningPk(eq("insert into dbtest (data_id, name, local_date, update_sequence, update_time) values (nextval('id_seq'),?,?,0,?)"), anyString()); 62 | verifyNoMoreInteractions(db); 63 | } 64 | 65 | @Test 66 | public void testFindByColumnOrder() throws Exception { 67 | // Configure the mock because our class under test expects values to be returned from the db 68 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 69 | .withColumnNames("name", "local_date", "update_sequence", "update_time") 70 | .addRow("Foo", localDateNow, 3, now)); 71 | 72 | // The test scenario 73 | RowStubMockData data = rowStubMockDao.findById(12L, RowStubMockDao.ColumnLookupType.BY_ORDER, false); 74 | 75 | // Verify object in memory is updated properly 76 | assertEquals(Long.valueOf(12L), data.getDataId()); 77 | assertEquals("Foo", data.getName()); 78 | assertEquals(localDateNow, data.getLocalDate()); 79 | assertEquals(Integer.valueOf(3), data.getUpdateSequence()); 80 | assertEquals(now, data.getUpdateTime()); 81 | 82 | // Verify database queries against golden copies 83 | verify(db).query(anyString(), eq("select name, local_date, update_sequence, update_time from dbtest where data_id=12")); 84 | verifyNoMoreInteractions(db); 85 | } 86 | 87 | @Test 88 | public void testFindByColumnNames() throws Exception { 89 | // Configure the mock because our class under test expects values to be returned from the db 90 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 91 | .withColumnNames("name", "local_date", "update_sequence", "update_time") 92 | .addRow("Foo", localDateNow, 3, now)); 93 | 94 | // The test scenario 95 | RowStubMockData data = rowStubMockDao.findById(13L, RowStubMockDao.ColumnLookupType.BY_NAME, false); 96 | 97 | // Verify object in memory is updated properly 98 | assertEquals(Long.valueOf(13L), data.getDataId()); 99 | assertEquals("Foo", data.getName()); 100 | assertEquals(localDateNow, data.getLocalDate()); 101 | assertEquals(Integer.valueOf(3), data.getUpdateSequence()); 102 | assertEquals(now, data.getUpdateTime()); 103 | 104 | // Verify database queries against golden copies 105 | verify(db).query(anyString(), eq("select name, local_date, update_sequence, update_time from dbtest where data_id=13")); 106 | verifyNoMoreInteractions(db); 107 | } 108 | 109 | @Test 110 | public void testFindAndLock() throws Exception { 111 | // Configure the mock because our class under test expects values to be returned from the db 112 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 113 | .withColumnNames("name", "local_date", "update_sequence", "update_time") 114 | .addRow("Foo", localDateNow, 3, now)); 115 | 116 | // The test scenario 117 | RowStubMockData data = rowStubMockDao.findById(15L, RowStubMockDao.ColumnLookupType.BY_NUMBER, true); 118 | 119 | // Verify object in memory is updated properly 120 | assertEquals(Long.valueOf(15L), data.getDataId()); 121 | assertEquals("Foo", data.getName()); 122 | assertEquals(localDateNow, data.getLocalDate()); 123 | assertEquals(Integer.valueOf(3), data.getUpdateSequence()); 124 | assertEquals(now, data.getUpdateTime()); 125 | 126 | // Verify database queries against golden copies 127 | verify(db).query(anyString(), eq("select name, local_date, update_sequence, update_time from dbtest where data_id=15 for update")); 128 | verifyNoMoreInteractions(db); 129 | } 130 | 131 | @Test 132 | public void testUpdate() throws Exception { 133 | // Configure the mock because our class under test expects values to be returned from the db 134 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 135 | .withColumnNames("name", "local_date", "update_sequence", "update_time") 136 | .addRow("Foo", localDateNow, 3, now)); 137 | Date before = new Date(now.getTime() - 5000); 138 | 139 | RowStubMockData data = new RowStubMockData(); 140 | data.setDataId(100L); 141 | data.setName("Foo"); 142 | data.setLocalDate(localDateNow); 143 | data.setUpdateSequence(13); 144 | data.setUpdateTime(before); 145 | rowStubMockDao.update(data, 23L); 146 | 147 | // Verify object in memory is updated properly 148 | assertEquals(Long.valueOf(100L), data.getDataId()); 149 | assertEquals("Foo", data.getName()); 150 | assertEquals(localDateNow, data.getLocalDate()); 151 | assertEquals(Integer.valueOf(14), data.getUpdateSequence()); 152 | assertEquals(now, data.getUpdateTime()); 153 | 154 | // Verify database queries against golden copies 155 | verify(db).update(eq("update dbtest set name=?, local_date=?, update_sequence=?, update_time=? where data_id=?"), anyString()); 156 | verifyNoMoreInteractions(db); 157 | } 158 | 159 | @Test 160 | public void testDelete() throws Exception { 161 | Date before = new Date(now.getTime() - 5000); 162 | 163 | RowStubMockData data = new RowStubMockData(); 164 | data.setDataId(100L); 165 | data.setName("Foo"); 166 | data.setUpdateSequence(13); 167 | data.setUpdateTime(before); 168 | rowStubMockDao.delete(data, 23L); 169 | 170 | // Verify object in memory is updated properly 171 | assertEquals(Long.valueOf(100L), data.getDataId()); 172 | assertEquals("Foo", data.getName()); 173 | assertEquals(Integer.valueOf(14), data.getUpdateSequence()); 174 | assertEquals(now, data.getUpdateTime()); 175 | 176 | // Verify database queries against golden copies 177 | verify(db).update(anyString(), eq("delete from dbtest where data_id=100")); 178 | verifyNoMoreInteractions(db); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/SampleDaoTest.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import com.github.susom.database.example.Sample; 4 | import com.github.susom.database.example.SampleDao; 5 | import java.util.Date; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.Mock; 10 | import org.mockito.MockitoAnnotations; 11 | 12 | import com.github.susom.database.DatabaseImpl; 13 | import com.github.susom.database.DatabaseMock; 14 | import com.github.susom.database.Flavor; 15 | import com.github.susom.database.OptionsOverride; 16 | import com.github.susom.database.RowStub; 17 | 18 | import static org.junit.Assert.*; 19 | import static org.mockito.Mockito.*; 20 | 21 | /** 22 | * Unit tests for the API for creating new visit IDs. 23 | */ 24 | public class SampleDaoTest { 25 | @Mock 26 | private DatabaseMock db; 27 | private final Date now = new Date(); 28 | private SampleDao sampleDao; 29 | 30 | @Before 31 | public void initializeMocks() { 32 | MockitoAnnotations.initMocks(this); 33 | 34 | // This is the key for database mocking. Explicitly create our DatabaseImpl and give it our mock. 35 | // Could use any Flavor here, though postgres/oracle are a little easier than derby because 36 | // they create single operations for insert returning. 37 | sampleDao = new SampleDao(new DatabaseImpl(db, new OptionsOverride(Flavor.postgresql) { 38 | @Override 39 | public Date currentDate() { 40 | // Use a local variable from the test so we can verify in a deterministic way 41 | return now; 42 | } 43 | })); 44 | } 45 | 46 | @Test 47 | public void testCreate() throws Exception { 48 | // Configure the mock because DAO expects the pk to be returned from the insert 49 | when(db.insertReturningPk(anyString(), anyString())).thenReturn(1L); 50 | 51 | Sample sample = new Sample(); 52 | sample.setName("Foo"); 53 | 54 | sampleDao.createSample(sample, 1L); 55 | 56 | // Verify object in memory is updated properly 57 | assertEquals(Long.valueOf(1L), sample.getSampleId()); 58 | assertEquals("Foo", sample.getName()); 59 | assertEquals(Integer.valueOf(0), sample.getUpdateSequence()); 60 | assertEquals(now, sample.getUpdateTime()); 61 | 62 | // Verify SQL executed against golden copies 63 | verify(db).insertReturningPk(eq("insert into sample (sample_id, sample_name, update_sequence, update_time) values (nextval('id_seq'),?,0,?)"), anyString()); 64 | verify(db).insert(eq("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id, is_deleted) values (?,?,0,?,?,'N')"), anyString()); 65 | verifyNoMoreInteractions(db); 66 | } 67 | 68 | @Test 69 | public void testFind() throws Exception { 70 | // Configure the mock because our class under test expects values to be returned from the db 71 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 72 | .withColumnNames("sample_name", "update_sequence", "update_time") 73 | .addRow("Foo", 3, now)); 74 | 75 | // The test scenario 76 | Sample sample = sampleDao.findSampleById(15L, false); 77 | 78 | // Verify object in memory is updated properly 79 | assertEquals(Long.valueOf(15L), sample.getSampleId()); 80 | assertEquals("Foo", sample.getName()); 81 | assertEquals(Integer.valueOf(3), sample.getUpdateSequence()); 82 | assertEquals(now, sample.getUpdateTime()); 83 | 84 | // Verify database queries against golden copies 85 | verify(db).query(anyString(), eq("select sample_name, update_sequence, update_time from sample where sample_id=15")); 86 | verifyNoMoreInteractions(db); 87 | } 88 | 89 | @Test 90 | public void testFindAndLock() throws Exception { 91 | // Configure the mock because our class under test expects values to be returned from the db 92 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 93 | .withColumnNames("sample_name", "update_sequence", "update_time") 94 | .addRow("Foo", 3, now)); 95 | 96 | // The test scenario 97 | Sample sample = sampleDao.findSampleById(15L, true); 98 | 99 | // Verify object in memory is updated properly 100 | assertEquals(Long.valueOf(15L), sample.getSampleId()); 101 | assertEquals("Foo", sample.getName()); 102 | assertEquals(Integer.valueOf(3), sample.getUpdateSequence()); 103 | assertEquals(now, sample.getUpdateTime()); 104 | 105 | // Verify database queries against golden copies 106 | verify(db).query(anyString(), eq("select sample_name, update_sequence, update_time from sample where sample_id=15 for update")); 107 | verifyNoMoreInteractions(db); 108 | } 109 | 110 | @Test 111 | public void testUpdate() throws Exception { 112 | // Configure the mock because our class under test expects values to be returned from the db 113 | when(db.query(anyString(), anyString())).thenReturn(new RowStub() 114 | .withColumnNames("sample_name", "update_sequence", "update_time") 115 | .addRow("Foo", 3, now)); 116 | Date before = new Date(now.getTime() - 5000); 117 | 118 | Sample sample = new Sample(); 119 | sample.setSampleId(100L); 120 | sample.setName("Foo"); 121 | sample.setUpdateSequence(13); 122 | sample.setUpdateTime(before); 123 | sampleDao.updateSample(sample, 23L); 124 | 125 | // Verify object in memory is updated properly 126 | assertEquals(Long.valueOf(100L), sample.getSampleId()); 127 | assertEquals("Foo", sample.getName()); 128 | assertEquals(Integer.valueOf(14), sample.getUpdateSequence()); 129 | assertEquals(now, sample.getUpdateTime()); 130 | 131 | // Verify database queries against golden copies 132 | verify(db).update(eq("update sample set sample_name=?, update_sequence=?, update_time=? where sample_id=?"), anyString()); 133 | verify(db).insert(eq("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id, is_deleted) values (?,?,?,?,?,'N')"), anyString()); 134 | verifyNoMoreInteractions(db); 135 | } 136 | 137 | @Test 138 | public void testDelete() throws Exception { 139 | Date before = new Date(now.getTime() - 5000); 140 | 141 | Sample sample = new Sample(); 142 | sample.setSampleId(100L); 143 | sample.setName("Foo"); 144 | sample.setUpdateSequence(13); 145 | sample.setUpdateTime(before); 146 | sampleDao.deleteSample(sample, 23L); 147 | 148 | // Verify object in memory is updated properly 149 | assertEquals(Long.valueOf(100L), sample.getSampleId()); 150 | assertEquals("Foo", sample.getName()); 151 | assertEquals(Integer.valueOf(14), sample.getUpdateSequence()); 152 | assertEquals(now, sample.getUpdateTime()); 153 | 154 | // Verify database queries against golden copies 155 | verify(db).update(anyString(), eq("delete from sample where sample_id=100")); 156 | verify(db).insert(eq("insert into sample_history (sample_id, sample_name, update_sequence, update_time, update_user_id, is_deleted) values (?,?,?,?,?,'Y')"), anyString()); 157 | verifyNoMoreInteractions(db); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/SqlArgsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import org.junit.Test; 4 | 5 | import com.github.susom.database.SqlArgs; 6 | 7 | import static org.junit.Assert.*; 8 | 9 | /** 10 | * Unit tests for the SqlArgs class. 11 | * 12 | * @author garricko 13 | */ 14 | public class SqlArgsTest { 15 | @Test 16 | public void testTidyColumnNames() throws Exception { 17 | assertArrayEquals(new String[] { "column_1", "column_2", "a", "a_2", "a_3", "a1" }, 18 | SqlArgs.tidyColumnNames(new String[] { null, "", " a ", "a ", "#!@#$_a","#!@#$_1" })); 19 | 20 | check("TheBest", "the_best"); 21 | } 22 | 23 | private void check(String input, String output) { 24 | assertArrayEquals(new String[] { output }, SqlArgs.tidyColumnNames(new String[] { input })); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/SqlServerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 The Board of Trustees of The Leland Stanford Junior University. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.susom.database.test; 18 | 19 | import org.junit.Ignore; 20 | import org.junit.Test; 21 | 22 | import com.github.susom.database.Config; 23 | import com.github.susom.database.ConfigFrom; 24 | import com.github.susom.database.DatabaseProvider; 25 | import com.github.susom.database.OptionsOverride; 26 | import com.github.susom.database.Schema; 27 | 28 | import static org.junit.Assert.assertArrayEquals; 29 | 30 | /** 31 | * Exercise Database functionality with a real Oracle database. 32 | * 33 | * @author garricko 34 | */ 35 | public class SqlServerTest extends CommonTest { 36 | @Override 37 | protected DatabaseProvider createDatabaseProvider(OptionsOverride options) throws Exception { 38 | String propertiesFile = System.getProperty("local.properties", "local.properties"); 39 | Config config = ConfigFrom.firstOf() 40 | .systemProperties() 41 | .propertyFile(propertiesFile) 42 | .excludePrefix("database.") 43 | .removePrefix("sqlserver.").get(); 44 | return DatabaseProvider.fromDriverManager(config) 45 | .withSqlParameterLogging() 46 | .withSqlInExceptionMessages() 47 | .withOptions(options).create(); 48 | } 49 | 50 | @Ignore("SQL Server prohibits NaN and Infinity") 51 | @Test 52 | public void argFloatNaN() { 53 | super.argFloatNaN(); 54 | } 55 | 56 | @Ignore("SQL Server prohibits NaN and Infinity") 57 | @Test 58 | public void argFloatInfinity() { 59 | super.argFloatInfinity(); 60 | } 61 | 62 | @Ignore("SQL Server prohibits NaN and Infinity") 63 | @Test 64 | public void argDoubleNaN() { 65 | super.argDoubleNaN(); 66 | } 67 | 68 | @Ignore("SQL Server prohibits NaN and Infinity") 69 | @Test 70 | public void argDoubleInfinity() { 71 | super.argDoubleInfinity(); 72 | } 73 | 74 | @Ignore("SQL Server seems to have incorrect min value for float (rounds to zero)") 75 | @Test 76 | public void argFloatMinMax() { 77 | super.argFloatMinMax(); 78 | } 79 | 80 | @Ignore("SQL Server doesn't support the interval syntax for date arithmetic") 81 | @Test 82 | public void intervals() { 83 | super.intervals(); 84 | } 85 | 86 | /** 87 | * SQL Server seems to have different behavior in that is does not convert 88 | * column names to uppercase (it preserves the case). 89 | * I haven't figured out how to smooth over this difference, since all databases 90 | * seem to respect the provided case when it is inside quotes, but don't provide 91 | * a way to tell whether a particular parameter was quoted. 92 | */ 93 | @Override 94 | @Test 95 | public void metadataColumnNames() { 96 | db.dropTableQuietly("dbtest"); 97 | 98 | new Schema().addTable("dbtest").addColumn("pk").primaryKey().schema().execute(db); 99 | 100 | db.toSelect("select Pk, Pk as Foo, Pk as \"Foo\" from dbtest").query(rs -> { 101 | assertArrayEquals(new String[] { "Pk", "Foo", "Foo" }, rs.getColumnLabels()); 102 | return null; 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/github/susom/database/test/VertxProviderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.susom.database.test; 2 | 3 | import static java.lang.Thread.sleep; 4 | import static org.junit.Assert.assertEquals; 5 | 6 | import com.github.susom.database.Config; 7 | import com.github.susom.database.ConfigFrom; 8 | import com.github.susom.database.DatabaseProviderVertx; 9 | import com.github.susom.database.DatabaseProviderVertx.Builder; 10 | import io.vertx.core.Vertx; 11 | import io.vertx.ext.unit.Async; 12 | import io.vertx.ext.unit.TestContext; 13 | import io.vertx.ext.unit.junit.VertxUnitRunner; 14 | import java.io.File; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import org.junit.Test; 18 | import org.junit.runner.RunWith; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | /** 23 | * Verify some asynchronous/blocking behavior of queries in the VertxProvider. 24 | */ 25 | @RunWith(VertxUnitRunner.class) 26 | public class VertxProviderTest { 27 | private final Logger log = LoggerFactory.getLogger(VertxProviderTest.class); 28 | 29 | static { 30 | // We will put all Derby related files inside ./build to keep our working copy clean 31 | File directory = new File("target").getAbsoluteFile(); 32 | if (directory.exists() || directory.mkdirs()) { 33 | System.setProperty("derby.stream.error.file", new File(directory, "derby.log").getAbsolutePath()); 34 | } 35 | } 36 | 37 | @Test 38 | public void testSlowOperationBlocking(TestContext context) { 39 | Async async = context.async(); 40 | 41 | Vertx vertx = Vertx.vertx(); 42 | 43 | // Set pool size to 1 so we can test blocking behavior: the 44 | // first request will block, and the rest will queue up 45 | Config config = ConfigFrom.firstOf() 46 | .value("database.url", "jdbc:derby:target/testdb;create=true") 47 | .value("database.pool.size", "1").get(); 48 | Builder dbb = DatabaseProviderVertx.pooledBuilder(vertx, config); 49 | 50 | List results = runQueries(vertx, dbb, async); 51 | 52 | async.awaitSuccess(); 53 | assertEquals(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19), results); 54 | vertx.close(); 55 | dbb.close(); 56 | } 57 | 58 | @Test 59 | public void testSlowOperationNonBlocking(TestContext context) { 60 | Async async = context.async(); 61 | 62 | Vertx vertx = Vertx.vertx(); 63 | 64 | // Set pool size to 2 so we can test blocking behavior: the 65 | // first request will block, and the rest will execute in order 66 | // while it is blocked 67 | Config config = ConfigFrom.firstOf() 68 | .value("database.url", "jdbc:derby:target/testdb;create=true") 69 | .value("database.pool.size", "2").get(); 70 | Builder dbb = DatabaseProviderVertx.pooledBuilder(vertx, config); 71 | 72 | List results = runQueries(vertx, dbb, async); 73 | 74 | async.awaitSuccess(); 75 | assertEquals(List.of(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1), results); 76 | vertx.close(); 77 | dbb.close(); 78 | } 79 | 80 | private List runQueries(Vertx vertx, Builder dbb, Async async) { 81 | List results = new ArrayList<>(); 82 | vertx.runOnContext(v -> { 83 | log.info("Running on the event loop thread"); 84 | dbb.transactAsync(db -> { 85 | log.info("Sleeping for 5s"); 86 | sleep(5000); 87 | log.info("Done sleeping"); 88 | return db.get().toSelect("values (1)").queryIntegerOrZero(); 89 | }, ar -> { 90 | log.info("Completed query: " + ar.result()); 91 | results.add(ar.result()); 92 | if (results.size() == 19) { 93 | async.complete(); 94 | } 95 | }); 96 | 97 | for (int i = 2; i < 20; i++) { 98 | int q = i; 99 | dbb.transactAsync(db -> 100 | db.get().toSelect("values (" + q + ")").queryIntegerOrZero() 101 | , ar -> { 102 | log.info("Completed query: " + ar.result()); 103 | results.add(ar.result()); 104 | if (results.size() == 19) { 105 | async.complete(); 106 | } 107 | }); 108 | } 109 | }); 110 | return results; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /test-oracle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PASSWORD=$(openssl rand -base64 18 | tr -d +/) 4 | #export TZ=Asia/Kolkata 5 | export TZ=America/Los_Angeles 6 | 7 | ORACLE_NAME=dbtest-ora 8 | 9 | stop_oracle() { 10 | docker rm -f $ORACLE_NAME 11 | } 12 | 13 | start_oracle() { 14 | docker pull us-west1-docker.pkg.dev/som-rit-infrastructure-prod/third-party/oracledb:$1 15 | # Supposedly we could set -e ORACLE_PWD=$PASSWORD here, but it doesn't seem to work 16 | docker run -d --rm --name $ORACLE_NAME -p 1521:1521 -p 5500:5500 us-west1-docker.pkg.dev/som-rit-infrastructure-prod/third-party/oracledb:$1 17 | 18 | if [ $? -ne 0 ] ; then 19 | echo "Unable to start Oracle docker ($1)" 20 | exit 1 21 | fi 22 | 23 | declare -i count=1 24 | while [ "$(docker inspect --format='{{json .State.Health.Status}}' $ORACLE_NAME)" != '"healthy"' ] 25 | do 26 | echo "Waiting for container to start ($count seconds)" 27 | sleep 1 28 | 29 | count=$((count + 1)) 30 | if [ $count -gt 120 ] ; then 31 | echo "Database did not startup correctly ($1)" 32 | stop_oracle 33 | exit 1 34 | fi 35 | done 36 | 37 | docker cp oracledb.sql dbtest-ora:/home/oracle/oracledb.sql 38 | docker exec $ORACLE_NAME sqlplus / AS SYSDBA @/home/oracle/oracledb.sql 39 | } 40 | 41 | test_oracle() { 42 | mvn -e -Dmaven.javadoc.skip=true \ 43 | -Dfailsafe.rerunFailingTestsCount=2 \ 44 | "-Ddatabase.url=jdbc:oracle:thin:@localhost:1521:ORCLCDB" \ 45 | -Ddatabase.user=testuser \ 46 | -Ddatabase.password="TestPassword456" \ 47 | -P oracle12.only,coverage test 48 | } 49 | 50 | run_oracle_tests() { 51 | start_oracle $1 52 | 53 | test_oracle 54 | 55 | if [ $? -ne 0 ] ; then 56 | echo "Test mvn command failed" 57 | stop_oracle 58 | exit 1 59 | fi 60 | 61 | stop_oracle 62 | } 63 | 64 | run_oracle_tests "19.3-quick" 65 | #run_oracle_tests 2019-latest -------------------------------------------------------------------------------- /test-postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PASSWORD=$(openssl rand -base64 18 | tr -d +/) 4 | #export TZ=Asia/Kolkata 5 | export TZ=America/Los_Angeles 6 | 7 | run_pg_tests() { 8 | docker pull postgres:$1 9 | docker run -d --rm --name dbtest-pg -e TZ=$TZ -e POSTGRES_PASSWORD=$PASSWORD -p 5432:5432/tcp postgres:$1 10 | 11 | declare -i count=1 12 | until docker exec dbtest-pg pg_isready -U postgres -h localhost -p 5432 > /dev/null 2>&1; 13 | do 14 | echo "Waiting for container to start ($count seconds)" 15 | sleep 1 16 | 17 | count=$((count + 1)) 18 | if [ $count -gt 120 ] ; then 19 | echo "Database did not startup correctly ($1)" 20 | docker rm -f dbtest-pg 21 | exit 1 22 | fi 23 | done 24 | 25 | mvn -e -Dmaven.javadoc.skip=true \ 26 | -Dfailsafe.rerunFailingTestsCount=2 \ 27 | -Dpostgres.database.url=jdbc:postgresql://localhost/postgres \ 28 | -Dpostgres.database.user=postgres \ 29 | -Dpostgres.database.password=$PASSWORD \ 30 | -P postgresql.only,coverage test 31 | 32 | if [ $? -ne 0 ] ; then 33 | echo "mvn command failed" 34 | docker rm -f dbtest-pg 35 | exit 1 36 | fi 37 | 38 | docker rm -f dbtest-pg 39 | } 40 | 41 | run_pg_tests 9.6 42 | run_pg_tests 10 43 | run_pg_tests 11 44 | run_pg_tests 12 45 | run_pg_tests 13 46 | run_pg_tests 14 47 | run_pg_tests 15 48 | run_pg_tests 16 49 | run_pg_tests 17 -------------------------------------------------------------------------------- /test-sqlserver.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PASSWORD=U.$(openssl rand -base64 18 | tr -d +/) 4 | #export TZ=Asia/Kolkata 5 | export TZ=America/Los_Angeles 6 | 7 | run_ms_tests() { 8 | docker pull mcr.microsoft.com/mssql/server:$1 9 | docker run -d --rm --name dbtest-ms -e ACCEPT_EULA=Y -e TZ=$TZ -e SA_PASSWORD=$PASSWORD -p 1433:1433 --health-cmd='/opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P '$PASSWORD' -Q "SELECT 1"' --health-interval=2s --health-timeout=30s --health-retries=5 mcr.microsoft.com/mssql/server:$1 10 | 11 | declare -i count=1 12 | while [ "$(docker inspect --format='{{json .State.Health.Status}}' dbtest-ms)" != '"healthy"' ] 13 | do 14 | echo "Waiting for container to start ($count seconds)" 15 | sleep 1 16 | 17 | count=$((count + 1)) 18 | if [ $count -gt 120 ] ; then 19 | echo "Database did not startup correctly ($1)" 20 | docker rm -f dbtest-ms 21 | exit 1 22 | fi 23 | done 24 | 25 | mvn -e -Dmaven.javadoc.skip=true \ 26 | -Dfailsafe.rerunFailingTestsCount=2 \ 27 | "-Dsqlserver.database.url=jdbc:sqlserver://localhost:1433" \ 28 | -Dsqlserver.database.user=sa \ 29 | -Dsqlserver.database.password=$PASSWORD \ 30 | -P sqlserver.only,coverage test 31 | 32 | if [ $? -ne 0 ] ; then 33 | echo "mvn command failed" 34 | docker rm -f dbtest-ms 35 | exit 1 36 | fi 37 | 38 | docker rm -f dbtest-ms 39 | } 40 | 41 | # The 2017 image seems to have a problem with daylight savings... 42 | #run_ms_tests 2017-latest 43 | run_ms_tests 2019-CU26-ubuntu-20.04 -------------------------------------------------------------------------------- /vagrant/postgresql-9.3/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "ubuntu/trusty64" 3 | config.vm.network "forwarded_port", guest: 5432, host: 5432 4 | config.vm.provision "shell", path: "box-init.sh" 5 | end 6 | -------------------------------------------------------------------------------- /vagrant/postgresql-9.3/box-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configure the package manager to find PostgreSQL 4 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 5 | sudo echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" > /etc/apt/sources 6 | 7 | # Install PostgreSQL, configure network access, and restart it 8 | sudo apt-get update && apt-get install -y postgresql-9.3 postgresql-contrib-9.3 9 | sudo -u postgres echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf 10 | sudo -u postgres echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf 11 | sudo service postgresql restart 12 | 13 | # Create a database user and database 14 | sudo -u postgres psql --command "CREATE USER vagrant WITH SUPERUSER PASSWORD 'vagrant';" 15 | sudo -u postgres createdb -O vagrant vagrant 16 | -------------------------------------------------------------------------------- /vagrant/postgresql-9.4/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "ubuntu/trusty64" 3 | config.vm.network "forwarded_port", guest: 5432, host: 5432 4 | config.vm.provision "shell", path: "box-init.sh" 5 | end 6 | -------------------------------------------------------------------------------- /vagrant/postgresql-9.4/box-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configure the package manager to find PostgreSQL 4 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 5 | sudo echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main 9.4" > /etc/apt/sources.list.d/pgdg.list 6 | 7 | # Install PostgreSQL, configure network access, and restart it 8 | sudo apt-get update && apt-get install -y postgresql-9.4 9 | sudo -u postgres echo "listen_addresses='*'" >> /etc/postgresql/9.4/main/postgresql.conf 10 | sudo -u postgres echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.4/main/pg_hba.conf 11 | sudo service postgresql restart 12 | 13 | # Create a database user and database 14 | sudo -u postgres psql --command "CREATE USER vagrant WITH SUPERUSER PASSWORD 'vagrant';" 15 | sudo -u postgres createdb -O vagrant vagrant 16 | -------------------------------------------------------------------------------- /vagrant/postgresql-9.5/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure("2") do |config| 2 | config.vm.box = "ubuntu/trusty64" 3 | config.vm.network "forwarded_port", guest: 5432, host: 5432 4 | config.vm.provision "shell", path: "box-init.sh" 5 | end 6 | -------------------------------------------------------------------------------- /vagrant/postgresql-9.5/box-init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Configure the package manager to find PostgreSQL 4 | sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 5 | sudo echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main 9.5" > /etc/apt/sources.list.d/pgdg.list 6 | 7 | # Install PostgreSQL, configure network access, and restart it 8 | sudo apt-get update && apt-get install -y postgresql-9.5 9 | sudo -u postgres echo "listen_addresses='*'" >> /etc/postgresql/9.5/main/postgresql.conf 10 | sudo -u postgres echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.5/main/pg_hba.conf 11 | sudo service postgresql restart 12 | 13 | # Create a database user and database 14 | sudo -u postgres psql --command "CREATE USER vagrant WITH SUPERUSER PASSWORD 'vagrant';" 15 | sudo -u postgres createdb -O vagrant vagrant 16 | --------------------------------------------------------------------------------