diff --git a/.github/workflows/integration-test-v1.yml b/.github/workflows/integration-test-v1.yml new file mode 100644 index 00000000..7a5dcc33 --- /dev/null +++ b/.github/workflows/integration-test-v1.yml @@ -0,0 +1,72 @@ +name: Run integration tests v1 + +on: + workflow_call: + inputs: + database: + description: 'Database - a new one will be created if not provided' + required: false + default: '' + type: string + engine: + description: 'Engine - a new one will be created if not provided' + required: false + type: string + secrets: + FIREBOLT_STG_USERNAME: + required: true + FIREBOLT_STG_PASSWORD: + required: true + +jobs: + run-integration-tests: + runs-on: ubuntu-latest + + steps: + - name: Validate database and engine + if: ${{ (inputs.database == '') != (inputs.engine == '') }} + uses: actions/github-script@v3 + with: + script: | + core.setFailed("Database and Engine parameters should be provided simultaneously") + + - name: Check out code + uses: actions/checkout@v3 + + - name: Prepare java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup database and engine + id: setup + if: ${{ inputs.database == '' }} + uses: firebolt-db/integration-testing-setup@v1 + with: + firebolt-username: ${{ secrets.FIREBOLT_STG_USERNAME }} + firebolt-password: ${{ secrets.FIREBOLT_STG_PASSWORD }} + api-endpoint: "api.staging.firebolt.io" + region: "us-east-1" + instance-type: "B2" + + - name: Determine database name + id: find-database-name + run: | + if ! [[ -z "${{ inputs.database }}" ]]; then + echo "database_name=${{ inputs.database }}" >> $GITHUB_OUTPUT + else + echo "database_name=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT + fi + + - name: Determine engine name + id: find-engine-name + run: | + if ! [[ -z "${{ inputs.engine }}" ]]; then + echo "engine_name=${{ inputs.engine }}" >> $GITHUB_OUTPUT + else + echo "engine_name=${{ steps.setup.outputs.engine_name }}" >> $GITHUB_OUTPUT + fi + + - name: Run integration tests + run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name }} -Dapi=api.staging.firebolt.io -Dpassword="${{ secrets.FIREBOLT_STG_PASSWORD }}" -Duser="${{ secrets.FIREBOLT_STG_USERNAME }}" -Dengine="${{ steps.find-engine-name.outputs.engine_name }}" -DexcludeTags=v2 diff --git a/.github/workflows/integration-test-v2.yml b/.github/workflows/integration-test-v2.yml new file mode 100644 index 00000000..1f0a1920 --- /dev/null +++ b/.github/workflows/integration-test-v2.yml @@ -0,0 +1,75 @@ +name: Run integration tests v2 + +on: + workflow_call: + inputs: + database: + description: 'Database - a new one will be created if not provided' + required: false + default: '' + type: string + engine: + description: 'Engine - a new one will be created if not provided' + required: false + type: string + account: + description: 'Account' + required: true + type: string + secrets: + FIREBOLT_CLIENT_ID_STG_NEW_IDN: + required: true + FIREBOLT_CLIENT_SECRET_STG_NEW_IDN: + required: true + +jobs: + run-integration-tests: + runs-on: ubuntu-latest + + steps: + - name: Validate database and engine + if: ${{ (inputs.database == '') != (inputs.engine == '') }} + uses: actions/github-script@v3 + with: + script: | + core.setFailed("Database and Engine parameters should be provided simultaneously") + + - name: Check out code + uses: actions/checkout@v3 + + - name: Prepare java + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: '17' + + - name: Setup database and engine + id: setup + if: ${{ inputs.database == '' }} + uses: firebolt-db/integration-testing-setup@v2 + with: + firebolt-client-id: ${{ secrets.FIREBOLT_CLIENT_ID_STG_NEW_IDN }} + firebolt-client-secret: ${{ secrets.FIREBOLT_CLIENT_SECRET_STG_NEW_IDN }} + account: ${{ inputs.account }} + api-endpoint: "api.staging.firebolt.io" + + - name: Determine database name + id: find-database-name + run: | + if ! [[ -z "${{ inputs.database }}" ]]; then + echo "database_name=${{ inputs.database }}" >> $GITHUB_OUTPUT + else + echo "database_name=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT + fi + + - name: Determine engine name + id: find-engine-name + run: | + if ! [[ -z "${{ inputs.engine }}" ]]; then + echo "engine_name=${{ inputs.engine }}" >> $GITHUB_OUTPUT + else + echo "engine_name=${{ steps.setup.outputs.engine_name }}" >> $GITHUB_OUTPUT + fi + + - name: Run integration tests + run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name }} -Denv=staging -Dclient_secret="${{ secrets.FIREBOLT_CLIENT_SECRET_STG_NEW_IDN }}" -Dclient_id="${{ secrets.FIREBOLT_CLIENT_ID_STG_NEW_IDN }}" -Daccount="${{ inputs.account }}" -Dengine="${{ steps.find-engine-name.outputs.engine_name }}" -DexcludeTags=v1 diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 26832431..9c99320b 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -3,78 +3,56 @@ name: Run integration tests on: workflow_dispatch: inputs: - database: - description: 'Database - a new one will be created if not provided' + database1: + description: 'Database (v1) - a new one will be created if not provided' required: false default: '' - environment: - description: 'Environment to run the tests against' + database-with-engine-v2: + description: 'Database (v2) for Firebolt v2 - a new one will be created if not provided' + required: false + default: '' + engine1: + description: 'Engine (v1) - a new one will be created if not provided' + required: false + engine_v2_fb_2_0: + description: 'Engine (v2) for Firebolt v2 - a new one will be created if not provided' + required: false + run-v1: + description: 'Run tests against Firebolt DB v1' + required: true + default: true type: choice + options: + - 'true' + - 'false' + run-database-with-engine-v2: + description: 'Run tests against Firebolt DB v2 and Engine V2' required: true - default: 'staging' + default: true + type: choice options: - - dev - - staging + - 'true' + - 'false' jobs: - run-integration-tests: - runs-on: ubuntu-latest - - steps: - - name: "Foresight: Collect Workflow Telemetry" - uses: runforesight/foresight-workflow-kit-action@v1 - if: ${{ always() }} - - name: Check out code - uses: actions/checkout@v3 - - - name: Prepare java - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: '17' - - name: Determine env variables - run: | - if [ "${{ github.event.inputs.environment }}" == 'staging' ]; then - echo "USERNAME=${{ secrets.FIREBOLT_USERNAME_STAGING }}" >> "$GITHUB_ENV" - echo "PASSWORD=${{ secrets.FIREBOLT_PASSWORD_STAGING }}" >> "$GITHUB_ENV" - echo "SERVICE_ACCOUNT_ID=${{ secrets.SERVICE_ACCOUNT_ID_STAGING }}" >> "$GITHUB_ENV" - echo "SERVICE_ACCOUNT_SECRET=${{ secrets.SERVICE_ACCOUNT_SECRET_STAGING }}" >> "$GITHUB_ENV" - else - echo "USERNAME=${{ secrets.FIREBOLT_USERNAME_DEV }}" >> "$GITHUB_ENV" - echo "PASSWORD=${{ secrets.FIREBOLT_PASSWORD_DEV }}" >> "$GITHUB_ENV" - echo "SERVICE_ACCOUNT_ID=${{ secrets.SERVICE_ACCOUNT_ID_DEV }}" >> "$GITHUB_ENV" - echo "SERVICE_ACCOUNT_SECRET=${{ secrets.SERVICE_ACCOUNT_SECRET_DEV }}" >> "$GITHUB_ENV" - fi - - name: Setup database and engine - id: setup - if: ${{ github.event.inputs.database == '' }} - uses: firebolt-db/integration-testing-setup@v1 - with: - firebolt-username: ${{ env.USERNAME }} - firebolt-password: ${{ env.PASSWORD }} - api-endpoint: "api.${{ github.event.inputs.environment }}.firebolt.io" - region: "us-east-1" - instance-type: "B2" - - - name: Determine database name - id: find-database-name - run: | - if ! [[ -z "${{ github.event.inputs.database }}" ]]; then - echo "database_name=${{ github.event.inputs.database }}" >> $GITHUB_OUTPUT - else - echo "database_name=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT - fi + run-integration-tests1: + if: ${{ inputs.run-v1 == 'true' }} + uses: ./.github/workflows/integration-test-v1.yml + with: + database: ${{ inputs.database1 }} + engine: ${{ inputs.engine1 }} + secrets: + FIREBOLT_STG_USERNAME: ${{ secrets.FIREBOLT_STG_USERNAME }} + FIREBOLT_STG_PASSWORD: ${{ secrets.FIREBOLT_STG_PASSWORD }} - - name: Run integration tests - run: ./gradlew integrationTest -Ddb=${{ steps.find-database-name.outputs.database_name }} -Dapi=api.${{ github.event.inputs.environment }}.firebolt.io -Dpassword="${{ env.SERVICE_ACCOUNT_SECRET }}" -Duser="${{ env.SERVICE_ACCOUNT_ID }}" + run-integration-tests-engine2: + if: ${{ inputs.run-database-with-engine-v2 == 'true' }} + uses: ./.github/workflows/integration-test-v2.yml + with: + database: ${{ inputs.database-with-engine-v2 }} + engine: ${{ inputs.engine_v2_fb_2_0 }} + account: ${{ vars.FIREBOLT_ACCOUNT_V2 }} + secrets: + FIREBOLT_CLIENT_ID_STG_NEW_IDN: ${{ secrets.FIREBOLT_CLIENT_ID_STG_NEW_IDN }} + FIREBOLT_CLIENT_SECRET_STG_NEW_IDN: ${{ secrets.FIREBOLT_CLIENT_SECRET_STG_NEW_IDN }} - - name: "Foresight: Analyze Test Results" - uses: runforesight/foresight-test-kit-action@v1 - if: success() || failure() - with: - test_format: JUNIT - test_framework: JUNIT - test_path: ./build/test-results/ - tags: | - type:"integration" - language:"Java" diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml index e7e7fc5b..31973ef9 100644 --- a/.github/workflows/performance-test.yml +++ b/.github/workflows/performance-test.yml @@ -36,11 +36,11 @@ jobs: path: dependencies - name: Setup database and engine id: setup - if: ${{ github.event.inputs.database == '' }} + if: ${{ inputs.database == '' }} uses: firebolt-db/integration-testing-setup@v1 with: - firebolt-username: ${{ secrets.FIREBOLT_USERNAME_STAGING }} - firebolt-password: ${{ secrets.FIREBOLT_PASSWORD_STAGING }} + firebolt-username: ${{ secrets.FIREBOLT_STG_USERNAME }} + firebolt-password: ${{ secrets.FIREBOLT_STG_PASSWORD }} api-endpoint: "api.staging.firebolt.io" region: "us-east-1" instance-type: "C2" @@ -49,8 +49,8 @@ jobs: - name: Determine database name id: find-database-name run: | - if ! [[ -z "${{ github.event.inputs.database }}" ]]; then - echo "database_name=${{ github.event.inputs.database }}" >> $GITHUB_OUTPUT + if ! [[ -z "${{ inputs.database }}" ]]; then + echo "database_name=${{ inputs.database }}" >> $GITHUB_OUTPUT else echo "database_name=${{ steps.setup.outputs.database_name }}" >> $GITHUB_OUTPUT fi @@ -61,7 +61,7 @@ jobs: testFilePath: JDBC.jmx dependencyFolder: dependencies outputReportsFolder: reports/ - args: -Jdatabase=${{ steps.find-database-name.outputs.database_name }} -Jpassword=${{ secrets.SERVICE_ACCOUNT_SECRET_STAGING }} -Jusername=${{ secrets.SERVICE_ACCOUNT_ID_STAGING }} -Jdriver=${{ needs.build.outputs.uber-jar }} -Jenvironment=staging -Jthreads=${{ github.event.inputs.threads }} -Jloops=${{ github.event.inputs.loops }} + args: -Jdatabase=${{ steps.find-database-name.outputs.database_name }} -Jpassword=${{ secrets.SERVICE_ACCOUNT_SECRET_STAGING }} -Jusername=${{ secrets.SERVICE_ACCOUNT_ID_STAGING }} -Jdriver=${{ needs.build.outputs.uber-jar }} -Jenvironment=staging -Jthreads=${{ inputs.threads }} -Jloops=${{ inputs.loops }} - name: Upload JMeter report uses: actions/upload-artifact@v3 with: @@ -78,7 +78,7 @@ jobs: - name: Extract test summary id: summary run: | - result="$(date +"%d-%m-%y %T %Z"),${{ github.event.inputs.threads }},$(jq -r '.Total.sampleCount, .Total.errorCount, .Total.errorPct, .Total.meanResTime, .Total.medianResTime, .Total.minResTime, .Total.maxResTime, .Total.pct1ResTime, .Total.pct2ResTime, .Total.pct3ResTime, .Total.throughput | round' reports/statistics.json| tr '\n' '|' | sed 's/|$/\n/'),${{ steps.short-sha.outputs.short_sha }},$GITHUB_REF" + result="$(date +"%d-%m-%y %T %Z"),${{ inputs.threads }},$(jq -r '.Total.sampleCount, .Total.errorCount, .Total.errorPct, .Total.meanResTime, .Total.medianResTime, .Total.minResTime, .Total.maxResTime, .Total.pct1ResTime, .Total.pct2ResTime, .Total.pct3ResTime, .Total.throughput | round' reports/statistics.json| tr '\n' '|' | sed 's/|$/\n/'),${{ steps.short-sha.outputs.short_sha }},$GITHUB_REF" echo "SUMMARY=$(echo "$result" | tr '|' ',')" >> $GITHUB_OUTPUT sudo rm -rf "/home/runner/work/jdbc/jdbc/reports" - name: Checkout history branch diff --git a/build.gradle b/build.gradle index 3b978679..c034379e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,10 +3,10 @@ import java.util.jar.JarFile plugins { id 'java' id 'jacoco' - id "org.sonarqube" version "4.4.1.3373" + id "org.sonarqube" version "5.0.0.4638" id 'maven-publish' id 'com.github.johnrengelman.shadow' version '8.0.0' - id 'org.quiltmc.gradle.licenser' version '2.0.1' + id 'org.quiltmc.gradle.licenser' version '2.0.2' id 'signing' } @@ -60,36 +60,36 @@ sourceSets { } dependencies { implementation 'com.google.code.findbugs:jsr305:3.0.2' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' - implementation 'com.squareup.okhttp3:okhttp:4.11.0' + implementation 'org.json:json:20240303' + + implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'net.jodah:expiringmap:0.5.11' - implementation 'org.apache.commons:commons-lang3:3.13.0' - implementation 'org.apache.commons:commons-text:1.10.0' + implementation 'org.apache.commons:commons-text:1.12.0' implementation 'org.lz4:lz4-java:1.8.0' - implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.slf4j:jul-to-slf4j:2.0.13' implementation fileTree(dir: 'libs', includes: ['*.jar']) - compileOnly 'org.slf4j:slf4j-api:2.0.9' - compileOnly 'org.projectlombok:lombok:1.18.30' - annotationProcessor 'org.projectlombok:lombok:1.18.30' - testCompileOnly 'org.projectlombok:lombok:1.18.30' - testAnnotationProcessor 'org.projectlombok:lombok:1.18.30' - - testImplementation 'ch.qos.logback:logback-classic:1.4.9' - testImplementation 'org.mockito:mockito-junit-jupiter:4.11.0' - testImplementation 'org.mockito:mockito-core:4.11.0' - testImplementation 'org.mockito:mockito-inline:4.11.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0' - testImplementation 'org.junit-pioneer:junit-pioneer:1.9.1' + compileOnly 'org.slf4j:slf4j-api:2.0.13' + compileOnly 'org.projectlombok:lombok:1.18.32' + annotationProcessor 'org.projectlombok:lombok:1.18.32' + testCompileOnly 'org.projectlombok:lombok:1.18.32' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' + + testImplementation 'ch.qos.logback:logback-classic:1.5.6' + testImplementation 'org.mockito:mockito-junit-jupiter:5.10.0' + testImplementation 'org.mockito:mockito-core:5.10.0' + testImplementation 'org.mockito:mockito-inline:5.2.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2' + testImplementation 'org.junit-pioneer:junit-pioneer:2.2.0' testImplementation 'org.hamcrest:hamcrest-library:2.2' - testImplementation 'com.squareup.okhttp3:mockwebserver:4.11.0' - testImplementation 'com.squareup.okhttp3:okhttp-tls:4.11.0' - testImplementation 'io.zonky.test:embedded-postgres:2.0.4' - testCompileOnly 'org.slf4j:slf4j-api:2.0.9' - testCommonImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0' + testImplementation 'com.squareup.okhttp3:okhttp-tls:4.12.0' + testImplementation 'io.zonky.test:embedded-postgres:2.0.7' + testCompileOnly 'org.slf4j:slf4j-api:2.0.13' + testCommonImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' testImplementation sourceSets.testCommon.output compileTestJava.dependsOn processTestResources jar.dependsOn processTestResources @@ -99,6 +99,7 @@ dependencies { compileTestCommonJava.dependsOn processTestResources compileTestCommonJava.dependsOn processIntegrationTestResources compileIntegrationTestJava.dependsOn processIntegrationTestResources + javadoc.options.addStringOption('Xdoclint:none', '-quiet') } test { @@ -106,12 +107,17 @@ test { maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 testLogging { events 'PASSED', 'FAILED', 'SKIPPED' + exceptionFormat = 'full' + showStackTraces = true } } tasks.register('integrationTest', Test) { description = 'Runs integration tests.' - useJUnitPlatform() + useJUnitPlatform() { + includeTags(System.getProperty("includeTags", "common").split(",")) + excludeTags(System.getProperty("excludeTags", "nothing").split(",")) + } reports { junitXml { outputPerTestCase = true // defaults to false @@ -125,9 +131,40 @@ tasks.register('integrationTest', Test) { testLogging.exceptionFormat = 'full' testClassesDirs = sourceSets.integrationTest.output.classesDirs classpath = sourceSets.integrationTest.runtimeClasspath - maxParallelForks = (int) (Runtime.runtime.availableProcessors() / 2 + 1) + // Temporarily disabled multithreaded execution due to bug on server side. + //maxParallelForks = (int) (Runtime.runtime.availableProcessors() / 2 + 1) + + testLogging { + events 'PASSED', 'FAILED', 'SKIPPED' + } } +allprojects { + // add a collection to track failedTests + ext.failedTests = [] + + // add a testlistener to all tasks of type Test + tasks.withType(Test) { + afterTest { TestDescriptor descriptor, TestResult result -> + if(result.resultType == org.gradle.api.tasks.testing.TestResult.ResultType.FAILURE){ + failedTests << ["${descriptor.className}::${descriptor.name}"] + } + } + } + + // print out tracked failed tests when the build has finished + gradle.buildFinished { + if(!failedTests.empty){ + println "Failed tests for ${project.name}:" + failedTests.each { failedTest -> + println failedTest + } + println "End of the list" + } else { + println "No failures found" + } + } +} jacoco { toolVersion = "0.8.10" diff --git a/gradle.properties b/gradle.properties index d9c342e2..749134fd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=2.4.6 +version=3.0.4-SNAPSHOT jdbcVersion=4.3 \ No newline at end of file diff --git a/lombok.config b/lombok.config index 86058d25..9ab6f2ab 100644 --- a/lombok.config +++ b/lombok.config @@ -1,4 +1,3 @@ lombok.anyConstructor.addConstructorProperties = true config.stopBubbling = true lombok.addLombokGeneratedAnnotation = true -lombok.log.custom.declaration = com.firebolt.jdbc.log.FireboltLogger com.firebolt.jdbc.util.LoggerUtil.getLogger(NAME) \ No newline at end of file diff --git a/src/integrationTest/java/integration/ConnectionInfo.java b/src/integrationTest/java/integration/ConnectionInfo.java index 9c1e2612..a72f643a 100644 --- a/src/integrationTest/java/integration/ConnectionInfo.java +++ b/src/integrationTest/java/integration/ConnectionInfo.java @@ -1,29 +1,110 @@ package integration; -import lombok.Value; - +import java.util.Objects; import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.lang.System.getProperty; +import static java.util.stream.Collectors.joining; -@Value public class ConnectionInfo { - private static ConnectionInfo INSTANCE; - String password; - String user; - String api; - String database; + private static final String JDBC_URL_PREFIX = "jdbc:firebolt:"; + private static volatile ConnectionInfo INSTANCE; + // principal and secret are used here instead of client_id and client_secret respectively as more common term also used in java security API. + private final String principal; + private final String secret; + private final String env; + private final String database; + private final String account; + private final String engine; + private final String api; + private final Supplier jdbcUrlSupplier; private ConnectionInfo() { - password = Optional.ofNullable(System.getProperty("password")).map(p -> p.replace("\"", "")).orElse(null); - user = Optional.ofNullable(System.getProperty("user")).map(u -> u.replace("\"", "")).orElse(null); - api = System.getProperty("api"); - database = System.getProperty("db"); + this( + getTrimmedProperty("client_id", "user"), + getTrimmedProperty("client_secret", "password"), + getProperty("env"), + getProperty("db"), + getProperty("account"), + getProperty("engine"), + getProperty("api") + ); + } + + public ConnectionInfo(String principal, String secret, String env, String database, String account, String engine, String api) { + this.principal = principal; + this.secret = secret; + this.env = env; + this.database = database; + this.account = account; + this.engine = engine; + this.api = api; + jdbcUrlSupplier = api == null ? this::toJdbcUrl2 : this::toJdbcUrl1; } public static ConnectionInfo getInstance() { if (INSTANCE == null) { - INSTANCE = new ConnectionInfo(); + synchronized (ConnectionInfo.class) { + if (INSTANCE == null) { + INSTANCE = new ConnectionInfo(); + } + } } return INSTANCE; } + private static String getTrimmedProperty(String name, String alias) { + return Optional.ofNullable(getProperty(name, getProperty(alias))).map(u -> u.replace("\"", "")).orElse(null); + } + + public String getPrincipal() { + return principal; + } + + public String getSecret() { + return secret; + } + + public String getEnv() { + return env; + } + + public String getDatabase() { + return database; + } + + public String getAccount() { + return account; + } + + public String getEngine() { + return engine; + } + + public String getApi() { + return api; + } + + public String toJdbcUrl() { + return jdbcUrlSupplier.get(); + } + + private String toJdbcUrl1() { + return "jdbc:firebolt://" + api + "/" + (database == null ? "" : database) + (engine == null ? "" : "?engine=" + engine); + } + + private String toJdbcUrl2() { + String params = Stream.of(param("env", env), param("engine", engine), param("account", account)).filter(Objects::nonNull).collect(joining("&")); + if (!params.isEmpty()) { + params = "?" + params; + } + return JDBC_URL_PREFIX + (database == null ? "" : database) + params; + } + + private String param(String name, String value) { + return value == null ? null : format("%s=%s", name, value); + } } diff --git a/src/integrationTest/java/integration/EnvironmentCondition.java b/src/integrationTest/java/integration/EnvironmentCondition.java new file mode 100644 index 00000000..c5ce0732 --- /dev/null +++ b/src/integrationTest/java/integration/EnvironmentCondition.java @@ -0,0 +1,63 @@ +package integration; + +import org.junit.jupiter.api.extension.ExtendWith; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(EnvironmentVersionCondition.class) +@Repeatable(EnvironmentConditions.class) +public @interface EnvironmentCondition { + String value(); + Attribute attribute() default Attribute.infraVersion; + Comparison comparison(); + enum Comparison { + LT { + boolean test(String value, String threshold) { + return compare(value, threshold) < 0; + } + }, + LE { + boolean test(String value, String threshold) { + return compare(value, threshold) <= 0; + } + }, + EQ { + boolean test(String value, String threshold) { + return compare(value, threshold) == 0; + } + }, + NE { + boolean test(String value, String threshold) { + return compare(value, threshold) != 0; + } + }, + GE { + boolean test(String value, String threshold) { + return compare(value, threshold) >= 0; + } + }, + GT { + boolean test(String value, String threshold) { + return compare(value, threshold) > 0; + } + }, + ; + + protected int compare(String value, String threshold) { + return value.compareTo(threshold); + } + abstract boolean test(String value, String threshold); + } + enum Attribute { + infraVersion, + databaseVersion, + protocolVersion, + fireboltVersion, + } +} diff --git a/src/integrationTest/java/integration/EnvironmentConditions.java b/src/integrationTest/java/integration/EnvironmentConditions.java new file mode 100644 index 00000000..9ddec430 --- /dev/null +++ b/src/integrationTest/java/integration/EnvironmentConditions.java @@ -0,0 +1,12 @@ +package integration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EnvironmentConditions { + EnvironmentCondition[] value(); +} diff --git a/src/integrationTest/java/integration/EnvironmentVersionCondition.java b/src/integrationTest/java/integration/EnvironmentVersionCondition.java new file mode 100644 index 00000000..0cc768f1 --- /dev/null +++ b/src/integrationTest/java/integration/EnvironmentVersionCondition.java @@ -0,0 +1,70 @@ +package integration; + +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.FireboltConnectionServiceSecret; +import com.firebolt.jdbc.connection.FireboltConnectionUserPassword; +import integration.EnvironmentCondition.Attribute; +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.opentest4j.AssertionFailedError; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Supplier; + +public class EnvironmentVersionCondition implements ExecutionCondition { + private static int infraVersion = -1; + private static String databaseVersion; + private static String protocolVersion; + private static String fireboltVersion; + + private static final Map, String> connectionVersions = Map.of( + FireboltConnectionUserPassword.class, "v1", + FireboltConnectionServiceSecret.class, "v2" + ); + + private static Map> attributeValueGetter = Map.of( + Attribute.infraVersion, () -> Integer.toString(infraVersion), + Attribute.databaseVersion, () -> databaseVersion, + Attribute.protocolVersion, () -> protocolVersion, + Attribute.fireboltVersion, () -> fireboltVersion + ); + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext extensionContext) { + Object testCase = extensionContext.getRequiredTestInstance(); + if (!(testCase instanceof IntegrationTest)) { + return ConditionEvaluationResult.enabled("Test enabled"); + } + EnvironmentCondition[] environmentConditions = extensionContext.getElement() + .map(a -> a.getAnnotationsByType(EnvironmentCondition.class)) + .orElse(new EnvironmentCondition[0]); + if (environmentConditions.length == 0) { + return ConditionEvaluationResult.enabled("Test enabled"); + } + if (infraVersion < 0) { + retrieveVersionAttributes((IntegrationTest)testCase); + } + + boolean enabled = Arrays.stream(environmentConditions) + .map(condition -> condition.comparison().test(attributeValueGetter.get(condition.attribute()).get(), condition.value())) + .filter(r -> r) + .findFirst() + .orElse(false); + return enabled ? ConditionEvaluationResult.enabled("Test enabled") : ConditionEvaluationResult.disabled("Test disabled"); + } + + private void retrieveVersionAttributes(IntegrationTest test) { + try (FireboltConnection conn = (FireboltConnection)test.createConnection()) { + infraVersion = conn.getInfraVersion(); + databaseVersion = conn.getMetaData().getDatabaseProductVersion(); + fireboltVersion = connectionVersions.get(conn.getClass()); + protocolVersion = conn.getProtocolVersion(); + } catch (SQLException e) { + throw new AssertionFailedError("Cannot establish connection", e); + } + } +} diff --git a/src/integrationTest/java/integration/IntegrationTest.java b/src/integrationTest/java/integration/IntegrationTest.java index 59fff8f1..06549081 100644 --- a/src/integrationTest/java/integration/IntegrationTest.java +++ b/src/integrationTest/java/integration/IntegrationTest.java @@ -1,8 +1,8 @@ package integration; import com.firebolt.jdbc.client.HttpClientConfig; -import lombok.CustomLog; import lombok.SneakyThrows; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.TestInstance; import java.io.InputStream; @@ -12,37 +12,35 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; -import java.util.Optional; +import static com.firebolt.jdbc.connection.FireboltConnectionUserPassword.SYSTEM_ENGINE_NAME; import static org.junit.jupiter.api.Assertions.assertNotNull; -@CustomLog @TestInstance(TestInstance.Lifecycle.PER_CLASS) +@Tag("common") public abstract class IntegrationTest { + private static final String JDBC_URL_PREFIX = "jdbc:firebolt:"; + protected Connection createLocalConnection(String queryParams) throws SQLException { return DriverManager.getConnection( - "jdbc:firebolt://localhost" + "/" + integration.ConnectionInfo.getInstance().getDatabase() - + queryParams, - integration.ConnectionInfo.getInstance().getUser(), - integration.ConnectionInfo.getInstance().getPassword()); + JDBC_URL_PREFIX + integration.ConnectionInfo.getInstance().getDatabase() + + queryParams + "&host=localhost" + getAccountParam(), + integration.ConnectionInfo.getInstance().getPrincipal(), + integration.ConnectionInfo.getInstance().getSecret()); } protected Connection createConnection() throws SQLException { - return DriverManager.getConnection( - "jdbc:firebolt://" + integration.ConnectionInfo.getInstance().getApi() + "/" - + integration.ConnectionInfo.getInstance().getDatabase(), - integration.ConnectionInfo.getInstance().getUser(), - integration.ConnectionInfo.getInstance().getPassword()); + return createConnection(integration.ConnectionInfo.getInstance().getEngine()); } protected Connection createConnection(String engine) throws SQLException { - return DriverManager.getConnection( - "jdbc:firebolt://" + integration.ConnectionInfo.getInstance().getApi() + "/" - + integration.ConnectionInfo.getInstance().getDatabase() - + Optional.ofNullable(engine).map(e -> "?engine=" + e).orElse(""), - integration.ConnectionInfo.getInstance().getUser(), - integration.ConnectionInfo.getInstance().getPassword()); + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + ConnectionInfo updated = new ConnectionInfo(current.getPrincipal(), current.getSecret(), + current.getEnv(), current.getDatabase(), current.getAccount(), engine, current.getApi()); + return DriverManager.getConnection(updated.toJdbcUrl(), + integration.ConnectionInfo.getInstance().getPrincipal(), + integration.ConnectionInfo.getInstance().getSecret()); } protected void setParam(Connection connection, String name, String value) throws SQLException { @@ -53,7 +51,7 @@ protected void setParam(Connection connection, String name, String value) throws @SneakyThrows protected void executeStatementFromFile(String path) { - executeStatementFromFile(path, null); + executeStatementFromFile(path, integration.ConnectionInfo.getInstance().getEngine()); } @SneakyThrows @@ -71,4 +69,11 @@ protected void removeExistingClient() throws NoSuchFieldException, IllegalAccess field.set(null, null); } + private String getAccountParam() { + return "&account=" + integration.ConnectionInfo.getInstance().getAccount(); + } + + protected String getSystemEngineName() { + return System.getProperty("api") == null ? null : SYSTEM_ENGINE_NAME; + } } diff --git a/src/integrationTest/java/integration/MockWebServerAwareIntegrationTest.java b/src/integrationTest/java/integration/MockWebServerAwareIntegrationTest.java new file mode 100644 index 00000000..20590475 --- /dev/null +++ b/src/integrationTest/java/integration/MockWebServerAwareIntegrationTest.java @@ -0,0 +1,31 @@ +package integration; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.IOException; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public abstract class MockWebServerAwareIntegrationTest extends IntegrationTest { + protected MockWebServer mockBackEnd; + + @BeforeEach + void setUp() throws IOException { + mockBackEnd = new MockWebServer(); + mockBackEnd.start(); + } + + @AfterEach + void tearDown() throws IOException { + mockBackEnd.close(); + } + + + protected void assertMockBackendRequestsCount(int expected) { + assertEquals(expected, mockBackEnd.getRequestCount()); + } +} diff --git a/src/integrationTest/java/integration/tests/ConnectionTest.java b/src/integrationTest/java/integration/tests/ConnectionTest.java new file mode 100644 index 00000000..2dd2289a --- /dev/null +++ b/src/integrationTest/java/integration/tests/ConnectionTest.java @@ -0,0 +1,154 @@ +package integration.tests; + +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.ExceptionType; +import com.firebolt.jdbc.exception.FireboltException; +import integration.ConnectionInfo; +import integration.EnvironmentCondition; +import integration.IntegrationTest; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class ConnectionTest extends IntegrationTest { + private int infraVersion; + + enum EngineType { + SYSTEM_ENGINE, CUSTOM_ENGINE + } + + @BeforeAll + void beforeAll() throws SQLException { + try (Connection conn = createConnection()) { + infraVersion = ((FireboltConnection) conn).getInfraVersion(); + } + } + + @ParameterizedTest(name = "{0}") + @Tag("v2") + @EnumSource(EngineType.class) + void connectToNotExistingDb(EngineType engineType) { + String database = "wrong_db"; + if (infraVersion >= 2) { + assumeTrue(EngineType.CUSTOM_ENGINE.equals(engineType)); + } + ConnectionInfo params = integration.ConnectionInfo.getInstance(); + String engineSuffix = EngineType.CUSTOM_ENGINE.equals(engineType) ? "&engine=" + params.getEngine() : ""; + String url = format("jdbc:firebolt:%s?env=%s&account=%s%s", database, params.getEnv(), params.getAccount(), engineSuffix); + FireboltException e = assertThrows(FireboltException.class, () -> DriverManager.getConnection(url, params.getPrincipal(), params.getSecret())); + if (infraVersion >= 2) { + assertEquals(ExceptionType.ERROR, e.getType()); + String expectedMessage = format("Database '%s' does not exist or not authorized", database); + assertTrue(e.getMessage().contains(expectedMessage), format("Error message '%s' does not match '%s'", e.getMessage(), expectedMessage)); + } else { + assertEquals(ExceptionType.RESOURCE_NOT_FOUND, e.getType()); + assertEquals(format("Database %s does not exist", database), e.getMessage()); + } + } + + /** + * Try to connect to existing DB and existing engine but the engine is attached to another DB. + * @throws SQLException if connection fails + */ + @Test + @Tag("v2") + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.LT) + void connectToWrongDbNotAttachedToEngine() throws SQLException { + ConnectionInfo params = integration.ConnectionInfo.getInstance(); + String enginelessDb = "engineless_db" + System.currentTimeMillis(); + try (Connection systemConnection = createConnection(null)) { + try { + systemConnection.createStatement().executeUpdate(format("CREATE DATABASE IF NOT EXISTS \"%s\"", enginelessDb)); + String url = format("jdbc:firebolt:%s?env=%s&account=%s&engine=%s", enginelessDb, params.getEnv(), params.getAccount(), params.getEngine()); + String errorMessage = format("The engine with the name %s is not attached to database %s", params.getEngine(), enginelessDb); + assertEquals(errorMessage, assertThrows(FireboltException.class, () -> DriverManager.getConnection(url, params.getPrincipal(), params.getSecret())).getMessage()); + } finally { + systemConnection.createStatement().executeUpdate(format("DROP DATABASE IF EXISTS %s", enginelessDb)); + } + } + } + + @ParameterizedTest(name = "using db:{0} engine:{1}") + @CsvSource({ + "true, false", + "true, true" + }) + @Tag("v1") + void successfulConnectV1(boolean useDatabase, boolean useEngine) throws SQLException { + successfulConnect(useDatabase, useEngine); + } + + @ParameterizedTest(name = "using db:{0} engine:{1}") + @CsvSource({ + "false, false", + }) + @Tag("v1") + void unsuccessfulConnectV1(boolean useDatabase, boolean useEngine) throws SQLException { + unsuccessfulConnect(useDatabase, useEngine); + } + + @ParameterizedTest(name = "using db:{0} engine:{1}") + @CsvSource({ + "false, true", // can connect but cannot execute select + }) + @Tag("v1") + void successfulConnectUnsuccessfulSelectV1(boolean useDatabase, boolean useEngine) throws SQLException { + ConnectionInfo params = integration.ConnectionInfo.getInstance(); + String url = getJdbcUrl(params, useDatabase, useEngine); + try (Connection connection = DriverManager.getConnection(url, params.getPrincipal(), params.getSecret()); + Statement statement = connection.createStatement()) { + assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1")); + } + } + + @ParameterizedTest(name = "V2 using db:{0} engine:{1}") + @CsvSource({ + "false, false", + "false, true", + "true, false", + "true, true" + + }) + @Tag("v2") + void successfulConnect(boolean useDatabase, boolean useEngine) throws SQLException { + ConnectionInfo params = integration.ConnectionInfo.getInstance(); + String url = getJdbcUrl(params, useDatabase, useEngine); + try (Connection connection = DriverManager.getConnection(url, params.getPrincipal(), params.getSecret()); + Statement statement = connection.createStatement()) { + ResultSet rs = statement.executeQuery("SELECT 1"); + assertTrue(rs.next()); + assertNotNull(rs.getObject(1)); + } + } + + void unsuccessfulConnect(boolean useDatabase, boolean useEngine) throws SQLException { + ConnectionInfo params = integration.ConnectionInfo.getInstance(); + String url = getJdbcUrl(params, useDatabase, useEngine); + assertThrows(FireboltException.class, () -> DriverManager.getConnection(url, params.getPrincipal(), params.getSecret())); + } + + private String getJdbcUrl(ConnectionInfo params, boolean useDatabase, boolean useEngine) { + String database = useDatabase ? params.getDatabase() : null; + String engine = useEngine ? params.getEngine() : null; + ConnectionInfo updated = new ConnectionInfo(params.getPrincipal(), params.getSecret(), params.getEnv(), database, params.getAccount(), engine, params.getApi()); + return updated.toJdbcUrl(); + } +} diff --git a/src/integrationTest/java/integration/tests/DatabaseMetaDataTest.java b/src/integrationTest/java/integration/tests/DatabaseMetaDataTest.java index a0b84128..ba83a7d5 100644 --- a/src/integrationTest/java/integration/tests/DatabaseMetaDataTest.java +++ b/src/integrationTest/java/integration/tests/DatabaseMetaDataTest.java @@ -2,9 +2,11 @@ import integration.ConnectionInfo; import integration.IntegrationTest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -14,9 +16,11 @@ import java.sql.Timestamp; import java.sql.Types; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.TreeMap; import static com.firebolt.jdbc.metadata.MetadataColumns.BUFFER_LENGTH; @@ -48,24 +52,42 @@ import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_CAT; import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_NAME; import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_SCHEM; +import static java.lang.String.format; +import static java.util.stream.Collectors.toList; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class DatabaseMetaDataTest extends IntegrationTest { - @BeforeEach + @BeforeAll void beforeAll() { executeStatementFromFile("/statements/metadata/ddl.sql"); } - @AfterEach + @AfterAll void afterEach() { executeStatementFromFile("/statements/metadata/cleanup.sql"); } + @Test + void getMetadata() throws SQLException { + try (Connection connection = createConnection()) { + DatabaseMetaData databaseMetaData = connection.getMetaData(); + assertNotNull(databaseMetaData); + assertSame(databaseMetaData, connection.getMetaData()); + connection.close(); + assertThat(assertThrows(SQLException.class, connection::getMetaData).getMessage(), containsString("closed")); + } + } + @Test void shouldReturnSchema() throws SQLException { List schemas = new ArrayList<>(); @@ -80,22 +102,20 @@ void shouldReturnSchema() throws SQLException { } } } - assertThat(schemas, containsInAnyOrder("public", "catalog", "information_schema")); + assertThat(schemas, containsInAnyOrder("public", "information_schema")); String dbName = ConnectionInfo.getInstance().getDatabase(); - assertThat(catalogs, contains(dbName, dbName, dbName)); + assertThat(catalogs, contains(dbName, dbName)); } @Test void shouldReturnTable() throws SQLException { Map> result = new HashMap<>(); - try (Connection connection = createConnection()) { - try (ResultSet rs = connection.getMetaData().getTables(connection.getCatalog(), "public", - "integration_test", null)) { - ResultSetMetaData metadata = rs.getMetaData(); - while (rs.next()) { - for (int i = 1; i <= metadata.getColumnCount(); i++) { - result.computeIfAbsent(metadata.getColumnName(i), k -> new ArrayList<>()).add(rs.getString(i)); - } + try (Connection connection = createConnection(); + ResultSet rs = connection.getMetaData().getTables(connection.getCatalog(), "public", "integration_test", null)) { + ResultSetMetaData metadata = rs.getMetaData(); + while (rs.next()) { + for (int i = 1; i <= metadata.getColumnCount(); i++) { + result.computeIfAbsent(metadata.getColumnName(i), k -> new ArrayList<>()).add(rs.getString(i)); } } } @@ -113,17 +133,56 @@ void shouldReturnTable() throws SQLException { assertNull(result.get(TYPE_NAME).get(0)); } + @ParameterizedTest + @CsvSource({ + // table types + ",,,,tables;views;integration_test,", + ",,,TABLE;VIEW,tables;views;integration_test,", + ",,,VIEW;TABLE,integration_test;tables;views,", + ",,,TABLE,integration_test,views;tables", + ",,,VIEW,views;tables,integration_test", + + // table name pattern + ",,%account%,,service_account_users;service_accounts,tables;columns;views", + ",,%test,,integration_test,tables;columns;views", + + // schema name pattern + ",public,,,integration_test,tables;columns;views", + ",information_schema,,,tables;columns,integration_test", + + // schema name pattern and table types + ",public,,TABLE,integration_test,tables;columns;views", + ",public,,TABLE;VIEW,integration_test,tables;columns;views", + ",public,,VIEW,,tables;columns;views", + ",information_schema,,TABLE,,integration_test", + ",information_schema,,TABLE;VIEW,tables;columns,integration_test", + ",information_schema,,VIEW,tables;columns,", + }) + void getTables(String catalog, String schemaPattern, String tableNamePattern, String typesStr, String expectedNamesStr, String unexpectedNamesStr) throws SQLException { + String[] types = typesStr == null ? null : typesStr.split(";"); + Collection expectedNames = expectedNamesStr == null ? Set.of() : Set.of(expectedNamesStr.split(";")); + Collection unexpectedNames = unexpectedNamesStr == null ? Set.of() : Set.of(unexpectedNamesStr.split(";")); + List names = new ArrayList<>(); + try (Connection connection = createConnection(); + ResultSet rs = connection.getMetaData().getTables(catalog, schemaPattern, tableNamePattern, types)) { + while (rs.next()) { + names.add(rs.getString(TABLE_NAME)); + } + } + assertTrue(names.containsAll(expectedNames), format("List %s does not contain expected items %s", names, expectedNames)); + List foundUnexpectedItems = names.stream().filter(unexpectedNames::contains).distinct().collect(toList()); + assertTrue(foundUnexpectedItems.isEmpty(), format("List %s contains unexpected items %s", names, foundUnexpectedItems)); + } + @Test void shouldReturnColumns() throws SQLException { Map> result = new HashMap<>(); - try (Connection connection = createConnection()) { - try (ResultSet rs = connection.getMetaData().getColumns(connection.getCatalog(), "public", - "integration_test", null)) { - ResultSetMetaData metadata = rs.getMetaData(); - while (rs.next()) { - for (int i = 1; i <= metadata.getColumnCount(); i++) { - result.computeIfAbsent(metadata.getColumnName(i), k -> new ArrayList<>()).add(rs.getString(i)); - } + try (Connection connection = createConnection(); + ResultSet rs = connection.getMetaData().getColumns(connection.getCatalog(), "public", "integration_test", null)) { + ResultSetMetaData metadata = rs.getMetaData(); + while (rs.next()) { + for (int i = 1; i <= metadata.getColumnCount(); i++) { + result.computeIfAbsent(metadata.getColumnName(i), k -> new ArrayList<>()).add(rs.getString(i)); } } } @@ -131,7 +190,7 @@ void shouldReturnColumns() throws SQLException { String tableName = "integration_test"; String schemaName = "public"; assertThat(result.get(SCOPE_TABLE), contains(null, null, null, null, null, null, null)); - assertThat(result.get(IS_NULLABLE), contains("NO", "YES", "YES", "YES", "YES", "YES", "NO")); + assertThat(result.get(IS_NULLABLE), contains("NO", "YES", "YES", "YES", "YES", "NO", "NO")); assertThat(result.get(BUFFER_LENGTH), contains(null, null, null, null, null, null, null)); assertThat(result.get(TABLE_CAT), contains(database, database, database, database, database, database, database)); assertThat(result.get(SCOPE_CATALOG), contains(null, null, null, null, null, null, null)); @@ -140,7 +199,7 @@ void shouldReturnColumns() throws SQLException { assertThat(result.get(COLUMN_NAME), contains("id", "ts", "tstz", "tsntz", "content", "success", "year")); assertThat(result.get(TABLE_SCHEM), contains(schemaName, schemaName, schemaName, schemaName, schemaName, schemaName, schemaName)); assertThat(result.get(REMARKS), contains(null, null, null, null, null, null, null)); - assertThat(result.get(NULLABLE), contains("0", "1", "1", "1", "1", "1", "0")); + assertThat(result.get(NULLABLE), contains("0", "1", "1", "1", "1", "0", "0")); assertThat(result.get(DECIMAL_DIGITS), contains("0", "0", "0", "0", "0", "0", "0")); assertThat(result.get(SQL_DATETIME_SUB), contains(null, null, null, null, null, null, null)); assertThat(result.get(NUM_PREC_RADIX), contains("10", "10", "10", "10", "10", "10", "10")); @@ -159,20 +218,19 @@ void shouldReturnColumns() throws SQLException { @Test void shouldReturnColumnsFromSelect() throws SQLException { Map> result = new TreeMap<>(); - try (Connection connection = createConnection()) { - try (ResultSet rs = connection.createStatement().executeQuery("select * from integration_test")) { - ResultSetMetaData metadata = rs.getMetaData(); - for (int i = 1; i <= metadata.getColumnCount(); i++) { - result.put(i, - new TreeMap<>(Map.of( - "type", metadata.getColumnType(i), - "typeName", metadata.getColumnTypeName(i), - "className", metadata.getColumnClassName(i), - "label", metadata.getColumnLabel(i), - "name", metadata.getColumnName(i), - "displaySize", metadata.getColumnDisplaySize(i) - ))); - } + try (Connection connection = createConnection(); + ResultSet rs = connection.createStatement().executeQuery("select * from integration_test")) { + ResultSetMetaData metadata = rs.getMetaData(); + for (int i = 1; i <= metadata.getColumnCount(); i++) { + result.put(i, + new TreeMap<>(Map.of( + "type", metadata.getColumnType(i), + "typeName", metadata.getColumnTypeName(i), + "className", metadata.getColumnClassName(i), + "label", metadata.getColumnLabel(i), + "name", metadata.getColumnName(i), + "displaySize", metadata.getColumnDisplaySize(i) + ))); } } diff --git a/src/integrationTest/java/integration/tests/MissingDataTest.java b/src/integrationTest/java/integration/tests/MissingDataTest.java new file mode 100644 index 00000000..7d4d37fc --- /dev/null +++ b/src/integrationTest/java/integration/tests/MissingDataTest.java @@ -0,0 +1,30 @@ +package integration.tests; + +import integration.ConnectionInfo; +import integration.IntegrationTest; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class MissingDataTest extends IntegrationTest { + @Test + @Tag("v2") + void missingAccount() throws SQLException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + try (Connection good = DriverManager.getConnection(current.toJdbcUrl(), current.getPrincipal(), current.getSecret())) { + assertNotNull(good); + } + + ConnectionInfo noAccount = new ConnectionInfo(current.getPrincipal(), current.getSecret(), + current.getEnv(), current.getDatabase(), null, current.getEngine(), current.getApi()); + assertThrows(SQLException.class, () -> DriverManager.getConnection(noAccount.toJdbcUrl(), noAccount.getPrincipal(), noAccount.getSecret())); + } +} diff --git a/src/integrationTest/java/integration/tests/NullableValuesTest.java b/src/integrationTest/java/integration/tests/NullableValuesTest.java index d4dec157..8ba1fae8 100644 --- a/src/integrationTest/java/integration/tests/NullableValuesTest.java +++ b/src/integrationTest/java/integration/tests/NullableValuesTest.java @@ -16,9 +16,9 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -public class NullableValuesTest extends IntegrationTest { +class NullableValuesTest extends IntegrationTest { @BeforeEach - void beforeAll() { + void beforeEach() { executeStatementFromFile("/statements/nullable-types/ddl.sql"); } diff --git a/src/integrationTest/java/integration/tests/NumericTypesTest.java b/src/integrationTest/java/integration/tests/NumericTypesTest.java new file mode 100644 index 00000000..0f7a3a5c --- /dev/null +++ b/src/integrationTest/java/integration/tests/NumericTypesTest.java @@ -0,0 +1,24 @@ +package integration.tests; + +import integration.IntegrationTest; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class NumericTypesTest extends IntegrationTest { + @Test + void shouldHaveCorrectInfo() throws SQLException { + try (Connection connection = createConnection(null); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT 3::decimal")) { + resultSet.next(); + assertEquals(9, resultSet.getMetaData().getScale(1)); + assertEquals(38, resultSet.getMetaData().getPrecision(1)); + } + } +} diff --git a/src/integrationTest/java/integration/tests/PreparedStatementArrayTest.java b/src/integrationTest/java/integration/tests/PreparedStatementArrayTest.java index 14350d1a..7a39e693 100644 --- a/src/integrationTest/java/integration/tests/PreparedStatementArrayTest.java +++ b/src/integrationTest/java/integration/tests/PreparedStatementArrayTest.java @@ -3,9 +3,9 @@ import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.type.array.FireboltArray; import integration.IntegrationTest; -import lombok.CustomLog; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -15,14 +15,17 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Statement; import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; -@CustomLog class PreparedStatementArrayTest extends IntegrationTest { enum PreparedStatementValueSetter { ARRAY { @@ -125,7 +128,31 @@ private void arrays(PreparedStatementValueSetter setter, Object intArray, Object } } - + @Test + void nullableBiDimensionalArray() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + try { + statement.executeUpdate("create table b6 (i int, x array(array(int NULL) NULL) NULL)"); + statement.executeUpdate("insert into b6 (i, x) values (0, [[1, 2], [3]]), (1, [[4, NULL, 5]]), (2, [[4], NULL, [5, NULL, 6]]), (3, NULL), (4, [[NULL,7]])"); + try (ResultSet rs = statement.executeQuery("select i, x from b6")) { + List result = new ArrayList<>(); + while(rs.next()) { + int index = rs.getInt(1); + Array value = rs.getArray(2); + Integer[][] values = value == null ? null : (Integer[][])value.getArray(); + result.add(index, values); + } + List expected = Arrays.asList(new Integer[][] {{1, 2}, {3}}, new Integer[][] {{4, null, 5}}, new Integer[][] {{4}, null, {5, null, 6}}, null, new Integer[][] {{null, 7}}); + assertEquals(expected.size(), result.size()); + for (int i = 0; i < expected.size(); i++) { + assertArrayEquals(expected.get(i), result.get(i)); + } + } + } finally { + statement.executeUpdate("drop table b6"); + } + } + } private void validateArrayUsingGetObject(ResultSet rs, int index, T[] expected) throws SQLException { assertSqlArray(rs.getObject(index), expected); diff --git a/src/integrationTest/java/integration/tests/PreparedStatementTest.java b/src/integrationTest/java/integration/tests/PreparedStatementTest.java index 2b5e3f39..090788c2 100644 --- a/src/integrationTest/java/integration/tests/PreparedStatementTest.java +++ b/src/integrationTest/java/integration/tests/PreparedStatementTest.java @@ -1,30 +1,54 @@ package integration.tests; -import static java.sql.Statement.SUCCESS_NO_INFO; -import static org.junit.jupiter.api.Assertions.*; - -import java.sql.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - +import com.firebolt.jdbc.CheckedBiFunction; +import com.firebolt.jdbc.CheckedTriFunction; import com.firebolt.jdbc.QueryResult; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.testutils.AssertionUtil; import com.firebolt.jdbc.type.FireboltDataType; - import integration.ConnectionInfo; import integration.IntegrationTest; import lombok.Builder; -import lombok.CustomLog; +import lombok.EqualsAndHashCode; import lombok.Value; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.StringReader; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static java.sql.Statement.SUCCESS_NO_INFO; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; -@CustomLog class PreparedStatementTest extends IntegrationTest { @BeforeEach @@ -59,7 +83,7 @@ void shouldInsertRecordsInBatch() throws SQLException { expectedRows.add(Arrays.asList(car1.getSales(), car1.getMake())); expectedRows.add(Arrays.asList(car2.getSales(), car2.getMake())); - QueryResult queryResult = this.createExpectedResult(expectedRows); + QueryResult queryResult = createExpectedResult(expectedRows); try (Statement statement = connection.createStatement(); ResultSet rs = statement @@ -70,6 +94,71 @@ void shouldInsertRecordsInBatch() throws SQLException { } } + Stream numericTypes() { + return Stream.of( + Arguments.of("byte", + (CheckedTriFunction) (s, i, v) -> { + s.setByte(i, v.byteValue()); + return null; + }, (CheckedBiFunction) (rs, i) -> (int) rs.getByte(i)), + + Arguments.of("short", + (CheckedTriFunction) (s, i, v) -> { + s.setShort(i, v.shortValue()); + return null; + }, (CheckedBiFunction) (rs, i) -> (int) rs.getShort(i)), + + Arguments.of("int", + (CheckedTriFunction) (s, i, v) -> { + s.setInt(i, v); + return null; + }, (CheckedBiFunction) (rs, i) -> (int) rs.getInt(i)), + + Arguments.of("long", + (CheckedTriFunction) (s, i, v) -> { + s.setLong(i, v.longValue()); + return null; + }, (CheckedBiFunction) (rs, i) -> (int) rs.getLong(i)), + + Arguments.of("getObject(Long.class)", + (CheckedTriFunction) (s, i, v) -> { + s.setLong(i, v.longValue()); + return null; + }, (CheckedBiFunction) (rs, i) -> rs.getObject(i, Long.class).intValue()), + + Arguments.of("getObject(i, java.util.Map.of(\"long\", Integer.class)", + (CheckedTriFunction) (s, i, v) -> { + s.setLong(i, v.longValue()); + return null; + }, (CheckedBiFunction) (rs, i) -> (int) rs.getObject(i, java.util.Map.of("long", Integer.class))) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("numericTypes") + void shouldInsertRecordsUsingDifferentNumericTypes(String name, CheckedTriFunction setter, CheckedBiFunction getter) throws SQLException { + Car car = Car.builder().make("Tesla").sales(42).build(); + try (Connection connection = createConnection()) { + + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make) VALUES (?,?)")) { + setter.apply(statement, 1, car.getSales()); + statement.setString(2, car.getMake()); + statement.executeUpdate(); + } + + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT sales, make FROM prepared_statement_test ORDER BY make")) { + assertTrue(rs.next()); + assertEquals(car.getSales(), getter.apply(rs, 1)); + assertEquals(car.getMake(), rs.getString(2)); + rs.getString(2); + + assertFalse(rs.next()); + } + } + } + @Test void shouldReplaceParamMarkers() throws SQLException { String insertSql = "INSERT INTO prepared_statement_test(sales, make) VALUES /* Some comment ? */ -- other comment ? \n (?,?)"; @@ -86,7 +175,7 @@ void shouldReplaceParamMarkers() throws SQLException { String selectSql = "SELECT sales, make FROM prepared_statement_test WHERE make = ?"; try (PreparedStatement selectStatement = connection.prepareStatement(selectSql)) { - QueryResult expectedResult = this.createExpectedResult(expectedRows); + QueryResult expectedResult = createExpectedResult(expectedRows); selectStatement.setString(1, "VW"); try (ResultSet rs = selectStatement.executeQuery(); ResultSet expectedRs = FireboltResultSet.of(expectedResult)) { @@ -162,20 +251,197 @@ void shouldFailSQLInjectionAttempt() throws SQLException { } } + @Test + void shouldInsertAndSelectByteArray() throws SQLException { + Car car1 = Car.builder().make("Ford").sales(12345).signature("Henry Ford".getBytes()).build(); + Car car2 = Car.builder().make("Tesla").sales(54321).signature("Elon Musk".getBytes()).build(); + try (Connection connection = createConnection()) { + + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make, signature) VALUES (?,?,?)")) { + statement.setLong(1, car1.getSales()); + statement.setString(2, car1.getMake()); + statement.setBytes(3, car1.getSignature()); + statement.addBatch(); + statement.setLong(1, car2.getSales()); + statement.setString(2, car2.getMake()); + statement.setBytes(3, car2.getSignature()); + statement.addBatch(); + int[] result = statement.executeBatch(); + assertArrayEquals(new int[] { SUCCESS_NO_INFO, SUCCESS_NO_INFO }, result); + } + + Set actual = new HashSet<>(); + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT sales, make, signature FROM prepared_statement_test")) { + while(rs.next()) { + actual.add(Car.builder().sales(rs.getInt(1)).make(rs.getString(2)).signature(rs.getBytes(3)).build()); + } + } + assertEquals(Set.of(car1, car2), actual); + } + } + + @Test + void shouldInsertAndSelectBlobClob() throws SQLException, IOException { + Car car1 = Car.builder().make("Ford").sales(12345).signature("Henry Ford".getBytes()).build(); + Car car2 = Car.builder().make("Tesla").sales(54321).signature("Elon Musk".getBytes()).build(); + try (Connection connection = createConnection()) { + + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make, signature) VALUES (?,?,?)")) { + statement.setLong(1, car1.getSales()); + statement.setClob(2, clob(connection, car1.getMake())); + statement.setBlob(3, blob(connection, car1.getSignature())); + statement.addBatch(); + statement.setLong(1, car2.getSales()); + statement.setClob(2, clob(connection, car2.getMake())); + statement.setBlob(3, blob(connection, car2.getSignature())); + statement.addBatch(); + int[] result = statement.executeBatch(); + assertArrayEquals(new int[] { SUCCESS_NO_INFO, SUCCESS_NO_INFO }, result); + } + + Set actual = new HashSet<>(); + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT sales, make, signature FROM prepared_statement_test")) { + while(rs.next()) { + actual.add(Car.builder() + .sales(rs.getInt(1)) + .make(new String(new BufferedReader(rs.getClob(2).getCharacterStream()).lines().collect(Collectors.joining(System.lineSeparator())))) + .signature(rs.getBlob(3).getBinaryStream().readAllBytes()) + .build()); + } + } + assertEquals(Set.of(car1, car2), actual); + } + } + + private Blob blob(Connection connection, byte[] bytes) throws SQLException { + Blob blob = connection.createBlob(); + blob.setBytes(1, bytes); + return blob; + } + + private Clob clob(Connection connection, String text) throws SQLException { + Clob clob = connection.createClob(); + clob.setString(1, text); + return clob; + } + + @Test + void shouldInsertAndSelectStreams() throws SQLException, IOException { + Car car1 = Car.builder().make("Ford").sales(12345).signature("Henry Ford".getBytes()).build(); + Car car2 = Car.builder().make("Tesla").sales(54321).signature("Elon Musk".getBytes()).build(); + try (Connection connection = createConnection()) { + + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make, signature) VALUES (?,?,?)")) { + statement.setLong(1, car1.getSales()); + statement.setCharacterStream(2, new StringReader(car1.getMake())); + statement.setBinaryStream(3, new ByteArrayInputStream(car1.getSignature())); + statement.addBatch(); + statement.setLong(1, car2.getSales()); + statement.setCharacterStream(2, new StringReader(car2.getMake())); + statement.setBinaryStream(3, new ByteArrayInputStream(car2.getSignature())); + statement.addBatch(); + int[] result = statement.executeBatch(); + assertArrayEquals(new int[] { SUCCESS_NO_INFO, SUCCESS_NO_INFO }, result); + } + + Set actual = new HashSet<>(); + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT sales, make, signature FROM prepared_statement_test")) { + while(rs.next()) { + actual.add(Car.builder() + .sales(rs.getInt(1)) + .make(new String(rs.getAsciiStream(2).readAllBytes())) + .signature(rs.getBinaryStream(3).readAllBytes()) + .build()); + } + } + assertEquals(Set.of(car1, car2), actual); + } + } + + @Test + void shouldInsertAndSelectDateTime() throws SQLException { + Car car1 = Car.builder().make("Ford").sales(12345).ts(new Timestamp(2)).d(new Date(3)).build(); + try (Connection connection = createConnection()) { + + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make, ts, d) VALUES (?,?,?,?)")) { + statement.setLong(1, car1.getSales()); + statement.setString(2, car1.getMake()); + statement.setTimestamp(3, car1.getTs()); + statement.setDate(4, car1.getD()); + statement.executeUpdate(); + } + + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT sales, make, ts, d FROM prepared_statement_test")) { + assertTrue(rs.next()); + Car actual = Car.builder().sales(rs.getInt(1)).make(rs.getString(2)).ts(rs.getTimestamp(3)).d(rs.getDate(4)).build(); + assertFalse(rs.next()); + // Date type in DB does not really hold the time, so the time part is unpredictable and cannot be compared. + // This is the reason to compare string representation of the object: Date.toString() returns the date only + // without hours, minutes and seconds. + assertEquals(car1.toString(), actual.toString()); + } + } + } + + private QueryResult createExpectedResult(List> expectedRows) { return QueryResult.builder().databaseName(ConnectionInfo.getInstance().getDatabase()) .tableName("prepared_statement_test") - .columns(Arrays.asList(QueryResult.Column.builder().name("sales").type(FireboltDataType.BIG_INT).build(), - QueryResult.Column.builder().name("make").type(FireboltDataType.TEXT).build())) + .columns( + Arrays.asList(QueryResult.Column.builder().name("sales").type(FireboltDataType.BIG_INT).build(), + QueryResult.Column.builder().name("make").type(FireboltDataType.TEXT).build())) .rows(expectedRows).build(); } + @Test + void shouldInsertAndRetrieveUrl() throws SQLException, MalformedURLException { + Car tesla = Car.builder().make("Tesla").url(new URL("https://www.tesla.com/")).sales(300).build(); + Car nothing = Car.builder().sales(0).build(); + try (Connection connection = createConnection()) { + try (PreparedStatement statement = connection + .prepareStatement("INSERT INTO prepared_statement_test (sales, make, url) VALUES (?,?,?)")) { + statement.setInt(1, tesla.getSales()); + statement.setString(2, tesla.getMake()); + statement.setURL(3, tesla.getUrl()); + statement.executeUpdate(); + statement.setInt(1, nothing.getSales()); + statement.setString(2, ""); + statement.setURL(3, null); + statement.executeUpdate(); + } + + try (Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("SELECT url FROM prepared_statement_test order by sales")) { + assertTrue(rs.next()); + assertNull(rs.getString(1)); + assertNull(rs.getURL(1)); + assertTrue(rs.next()); + assertEquals("https://www.tesla.com/", rs.getString(1)); + assertEquals(new URL("https://www.tesla.com/"), rs.getURL(1)); + assertFalse(rs.next()); + } + } + } + @Builder @Value + @EqualsAndHashCode private static class Car { Integer sales; String make; + byte[] signature; + Timestamp ts; + Date d; + URL url; } } diff --git a/src/integrationTest/java/integration/tests/SpecialValuesTest.java b/src/integrationTest/java/integration/tests/SpecialValuesTest.java new file mode 100644 index 00000000..82311021 --- /dev/null +++ b/src/integrationTest/java/integration/tests/SpecialValuesTest.java @@ -0,0 +1,175 @@ +package integration.tests; + +import integration.EnvironmentCondition; +import integration.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class SpecialValuesTest extends IntegrationTest { + private Connection systemConnection; + private Connection userConnection; + + @BeforeAll + void beforeAll() throws SQLException { + systemConnection = createConnection(getSystemEngineName()); + userConnection = createConnection(); + } + + @AfterAll + void afterAll() throws SQLException { + systemConnection.close(); + userConnection.close(); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::float", "select '+inf'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.LT) + void infFloatUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::real", "select '+inf'::real"}) + void infRealUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::real", "select '+inf'::real"}) + void infRealSystemEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::float", "select '+inf'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void infFloatAsDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::float", "select '+inf'::float"}) + void infFloatAsDoubleSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::double", "select '+inf'::double"}) + void infDoubleSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'inf'::double", "select '+inf'::double"}) + void infDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.LT) + void minusInfFloatUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void minusInfFloatAsDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::real"}) + void minusInfRealUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::real"}) + void minusInfRealSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::float"}) + void minusInfFloatAsDoubleSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select '-inf'::double"}) + void minusInfDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'nan'::float", "select '+nan'::float", "select '-nan'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.LT) + void nanFloatUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NaN, Double.NaN, Float.NaN); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'nan'::real", "select '+nan'::real", "select '-nan'::real"}) + void nanRealUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NaN, Double.NaN, Float.NaN); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'nan'::real", "select '+nan'::real", "select '-nan'::real"}) + void nanRealSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.NaN, Double.NaN, Float.NaN); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'nan'::float", "select '+nan'::float", "select '-nan'::float"}) + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void nanFloatAsDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NaN, Double.NaN, Double.NaN); + } + + @ParameterizedTest + @ValueSource(strings = { + "select 'nan'::double", "select '+nan'::double", "select '-nan'::double" + }) + void nanDoubleSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.NaN, Double.NaN, Double.NaN); + } + + @ParameterizedTest + @ValueSource(strings = { + "select 'nan'::float", "select '+nan'::float", "select '-nan'::float", + }) + void nanFloatAsDoubleSystemEngine(String query) throws SQLException { + specialSelect(systemConnection, query, Float.NaN, Double.NaN, Double.NaN); + } + + @ParameterizedTest + @ValueSource(strings = {"select 'nan'::double", "select '+nan'::double", "select '-nan'::double"}) + void nanDoubleUserEngine(String query) throws SQLException { + specialSelect(userConnection, query, Float.NaN, Double.NaN, Double.NaN); + } + + private void specialSelect(Connection connection, String query, Number floatGetObjectValue, Number doubleGetObjectValue, Number expectedGetObjectValue) throws SQLException { + try (Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(query)) { + resultSet.next(); + assertThrows(SQLException.class, () -> resultSet.getShort(1)); + assertThrows(SQLException.class, () -> resultSet.getInt(1)); + assertThrows(SQLException.class, () -> resultSet.getLong(1)); + assertEquals(floatGetObjectValue, resultSet.getFloat(1)); + assertEquals(doubleGetObjectValue, resultSet.getDouble(1)); + assertEquals(expectedGetObjectValue, resultSet.getObject(1)); + } + } +} diff --git a/src/integrationTest/java/integration/tests/StatementCancelTest.java b/src/integrationTest/java/integration/tests/StatementCancelTest.java index 2f42ed7d..83a01c73 100644 --- a/src/integrationTest/java/integration/tests/StatementCancelTest.java +++ b/src/integrationTest/java/integration/tests/StatementCancelTest.java @@ -1,28 +1,30 @@ package integration.tests; -import static org.junit.jupiter.api.Assertions.assertEquals; +import com.firebolt.jdbc.exception.ExceptionType; +import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.statement.FireboltStatement; +import integration.EnvironmentCondition; +import integration.IntegrationTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import com.firebolt.jdbc.exception.ExceptionType; -import com.firebolt.jdbc.exception.FireboltException; -import com.firebolt.jdbc.statement.FireboltStatement; - -import integration.IntegrationTest; -import lombok.CustomLog; +import static integration.EnvironmentCondition.Attribute.databaseVersion; +import static integration.EnvironmentCondition.Comparison.GE; +import static org.junit.jupiter.api.Assertions.assertEquals; -@CustomLog class StatementCancelTest extends IntegrationTest { + private static final Logger log = Logger.getLogger(StatementCancelTest.class.getName()); @BeforeEach void beforeEach() { @@ -36,21 +38,32 @@ void afterEach() { @Test @Timeout(value = 2, unit = TimeUnit.MINUTES) - void shouldCancelQuery() throws SQLException, InterruptedException { - try (Connection connection = createConnection()) { - long totalRecordsToInsert; - try (Statement statement = connection.createStatement()) { - ResultSet resultSet = statement.executeQuery("SELECT COUNT(*) from ex_lineitem"); - resultSet.next(); - totalRecordsToInsert = resultSet.getInt(1); - } + @Tag("v1") // generate_series is supported on all available engine of v2 + @Tag("slow") + void shouldCancelQueryV1() throws SQLException, InterruptedException { + shouldCancelQuery(); + } + + @Test + @Timeout(value = 10, unit = TimeUnit.MINUTES) + @EnvironmentCondition(value = "3.33", attribute = databaseVersion, comparison = GE) // generate_series is supported starting from version 3.33 on v2 + @Tag("v2") + @Tag("slow") + void shouldCancelQueryV2() throws SQLException, InterruptedException { + shouldCancelQuery(); + } + + private void shouldCancelQuery() throws SQLException, InterruptedException { + try (Connection connection = createConnection(); Statement fillStatement = connection.createStatement()) { + long now = System.currentTimeMillis(); + fillStatement.execute("insert into ex_lineitem ( l_orderkey ) SELECT * FROM GENERATE_SERIES(1, 100000000)"); + long insertTime = System.currentTimeMillis() - now; - try (FireboltStatement insertStatement = (FireboltStatement) connection.createStatement()) { + try (Statement insertStatement = connection.createStatement()) { Thread thread = new Thread(() -> { try { - insertStatement.execute( - "INSERT INTO first_statement_cancel_test SELECT * FROM ex_lineitem; INSERT INTO second_statement_cancel_test SELECT * FROM ex_lineitem;"); + insertStatement.execute("INSERT INTO first_statement_cancel_test SELECT * FROM ex_lineitem; INSERT INTO second_statement_cancel_test SELECT * FROM ex_lineitem;"); } catch (FireboltException e) { if (!e.getType().equals(ExceptionType.CANCELED)) { throw new RuntimeException(e); @@ -61,47 +74,56 @@ void shouldCancelQuery() throws SQLException, InterruptedException { } }); thread.start(); - while (!insertStatement.isStatementRunning()) { - Thread.sleep(1000); + // Wait until copying started + while (!((FireboltStatement)insertStatement).isStatementRunning()) { + Thread.sleep(100); } - insertStatement.cancel(); + Thread.sleep(insertTime / 10); // wait 10% of time that was spent to fill data to give chance to the insert statement to copy data + insertStatement.cancel(); // now cancel the statement } - Thread.sleep(5000); - verifyThatNoMoreRecordsAreAdded(connection, "first_statement_cancel_test", totalRecordsToInsert); + verifyThatNoMoreRecordsAreAdded(connection, "first_statement_cancel_test", insertTime); verifyThatSecondStatementWasNotExecuted(connection, "second_statement_cancel_test"); - } } - private void verifyThatNoMoreRecordsAreAdded(Connection connection, String tableName, long totalRecordsToInsert) - throws SQLException, InterruptedException { - String countAddedRecordsQuery = String.format("SELECT COUNT(*) FROM %s", tableName); - try (Statement countStatement = connection.createStatement()) { - ResultSet rs = countStatement.executeQuery(countAddedRecordsQuery); - rs.next(); - long count = rs.getInt(1); - log.info("{} records were added to table {} before the statement got cancelled", count, tableName); - Thread.sleep(5000); // waiting to see if more records are being added - rs = countStatement.executeQuery(countAddedRecordsQuery); - rs.next(); - assertEquals(count, rs.getInt(1)); - // The dataset is too small so all the data might already be ingested - // assertTrue(count <= totalRecordsToInsert, "No new records were added - // following the cancellation"); - rs.close(); + private void verifyThatNoMoreRecordsAreAdded(Connection connection, String tableName, long insertTime) throws SQLException, InterruptedException { + // Get number of rows in the table. Do it several times until we get something. Wait for 10% of time that spent to fill the table. + // We need several attempts because this DB does not support transactions, so sometimes it takes time until the + // data is available. + long waitForResultTime = insertTime / 2; + long waitForResultDelay = waitForResultTime / 10; + log.log(Level.INFO, "verifyThatNoMoreRecordsAreAdded insertTime={0}, waitForResultTime={0}", new Object[] {insertTime, waitForResultTime}); + int count0; + int i = 0; + for (count0 = count(connection, tableName); i < 10; count0 = count(connection, tableName), i++) { + log.log(Level.INFO, "verifyThatNoMoreRecordsAreAdded count0={0}", count0); + if (count0 > 0) { + break; + } + Thread.sleep(waitForResultDelay); } + // Wait for more time that we spent to fill the table. + // We want to wait enough to give a chance to the query to fill more data. + Thread.sleep(insertTime); // waiting to see if more records are being added + int count1 = count(connection, tableName); + Thread.sleep(insertTime); // waiting to see if more records are being added + int count2 = count(connection, tableName); + log.log(Level.INFO, "verifyThatNoMoreRecordsAreAdded count1={0}, count2={1}", new Object[] {count1, count2}); + assertEquals(count1, count2); } - private void verifyThatSecondStatementWasNotExecuted(Connection connection, String tableName) throws SQLException { + private int count(Connection connection, String tableName) throws SQLException { String countAddedRecordsQuery = String.format("SELECT COUNT(*) FROM %s", tableName); - try (Statement countStatement = connection.createStatement()) { - ResultSet rs = countStatement.executeQuery(countAddedRecordsQuery); - rs.next(); - assertEquals(0, rs.getInt(1)); + try (Statement countStatement = connection.createStatement(); ResultSet rs = countStatement.executeQuery(countAddedRecordsQuery)) { + return rs.next() ? rs.getInt(1) : 0; } } + private void verifyThatSecondStatementWasNotExecuted(Connection connection, String tableName) throws SQLException { + assertEquals(0, count(connection, tableName)); + } + /** * Extract table name when non-standard sql is used */ @@ -111,11 +133,10 @@ private String extractTableNameWithNonStandardSql(Connection connection, String ResultSet resultSet = statement.executeQuery("SHOW tables"); while (resultSet.next() && tableName == null) { log.info(resultSet.getString(1)); - if (StringUtils.startsWith(resultSet.getString(1), testTableName) - && StringUtils.endsWith(resultSet.getString(1), "_distributed")) { + String rsTableName = resultSet.getString(1); + if (rsTableName != null && rsTableName.startsWith(testTableName) && rsTableName.endsWith("_distributed")) { tableName = resultSet.getString(1); } - } } return tableName; diff --git a/src/integrationTest/java/integration/tests/StatementTest.java b/src/integrationTest/java/integration/tests/StatementTest.java index 455633be..2ed7ccc5 100644 --- a/src/integrationTest/java/integration/tests/StatementTest.java +++ b/src/integrationTest/java/integration/tests/StatementTest.java @@ -1,26 +1,40 @@ package integration.tests; +import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.FireboltException; +import integration.ConnectionInfo; +import integration.EnvironmentCondition; import integration.IntegrationTest; import kotlin.collections.ArrayDeque; -import lombok.CustomLog; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.ResultSet; +import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import java.util.stream.IntStream; import static java.lang.String.format; +import static java.sql.Statement.SUCCESS_NO_INFO; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -28,7 +42,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -@CustomLog class StatementTest extends IntegrationTest { @BeforeEach @@ -41,9 +54,37 @@ void afterEach() { executeStatementFromFile("/statements/statement/cleanup.sql"); } + @Test + void shouldSelect1() throws SQLException { + try (Connection connection = createConnection(); + ResultSet rs = connection.createStatement().executeQuery("SELECT 1")) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertFalse(rs.next()); + } + } + + @Test + @EnabledIfSystemProperty(named = "engine", matches = ".+") + void shouldSelect1WithEngine() throws SQLException { + try (Connection connection = createConnection(System.getProperty("engine")); Statement statement = connection.createStatement()) { + statement.executeQuery("SELECT 1;"); + assertNotNull(statement.executeQuery("SELECT 1;")); + } + } + + @Test + void shouldSelect1WithQueryTimeout() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.setQueryTimeout(10); // 10 seconds + statement.executeQuery("SELECT 1;"); + assertNotNull(statement.executeQuery("SELECT 1;")); + } + } + @Test void shouldReuseStatementWhenNotCloseOnCompletion() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { statement.executeQuery("SELECT 1;"); assertNotNull(statement.executeQuery("SELECT 1;")); } @@ -51,7 +92,7 @@ void shouldReuseStatementWhenNotCloseOnCompletion() throws SQLException { @Test void shouldThrowExceptionWhenTryingToReuseStatementClosedOnCompletion() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { statement.closeOnCompletion(); statement.executeQuery("SELECT 1;"); assertTrue(statement.isCloseOnCompletion()); @@ -59,23 +100,42 @@ void shouldThrowExceptionWhenTryingToReuseStatementClosedOnCompletion() throws S } } + @Test + void shouldThrowExceptionWhenExecutingWrongQuery() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + String errorMessage = assertThrows(FireboltException.class, () -> statement.executeQuery("select wrong query")).getMessage(); + assertTrue(errorMessage.contains("Column 'wrong' does not exist.")); + } + } + + @Test + @EnvironmentCondition(value = "4.2.0", comparison = EnvironmentCondition.Comparison.GE) + void shouldThrowExceptionWhenExecutingWrongQueryWithJsonError() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.execute("set advanced_mode=1"); + statement.execute("set enable_json_error_output_format=true"); + String errorMessage = assertThrows(FireboltException.class, () -> statement.executeQuery("select wrong query")).getMessage(); + assertTrue(errorMessage.contains("Column 'wrong' does not exist.")); + } + } + @Test void shouldReturnTrueWhenExecutingAStatementThatReturnsAResultSet() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { assertTrue(statement.execute("SELECT 1;")); } } @Test void shouldReturnTrueWhenExecutingMultiStatementWithFirstStatementReturningAResultSet() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { assertTrue(statement.execute("SELECT 1;")); } } @Test void shouldReturnFalseWhenExecutingMultiStatementWithFirstStatementNotReturningAResultSet() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { assertFalse(statement.execute("INSERT INTO statement_test(id) values (1); SELECT 1;")); } } @@ -110,9 +170,20 @@ private List selectIntValues(Connection connection, int limit) throws S return result; } + @Test + void shouldExecuteBatch() throws SQLException { + int size = 10; + try (Connection connection = createConnection(); Statement insert = connection.createStatement()) { + for (int i = 0; i < size; i++) { + insert.addBatch(format("INSERT INTO statement_test(id) values (%d)", i)); + } + assertArrayEquals(IntStream.generate(() -> SUCCESS_NO_INFO).limit(size).toArray(), insert.executeBatch()); + } + } + @Test void shouldThrowExceptionWhenTryingToExecuteQueryThatWouldReturnMultipleResultSets() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { assertThrows(FireboltException.class, () -> statement.executeQuery("SELECT 1; SELECT 2;")); } } @@ -159,8 +230,6 @@ void shouldNotCloseStatementWithCloseOnCompletionIfItHasMoreResults() throws SQL void shouldGetBooleans() throws SQLException { try (Connection connection = createConnection()) { try (Statement statement = connection.createStatement()) { - statement.execute( - "SET advanced_mode=1; SET output_format_firebolt_type_names=1; SET bool_output_format=postgres;"); ResultSet resultSet = statement.executeQuery("SELECT true, false, null::boolean;"); resultSet.next(); assertEquals(Boolean.TRUE, resultSet.getObject(1)); @@ -172,7 +241,83 @@ void shouldGetBooleans() throws SQLException { assertNull(resultSet.getObject(3)); assertFalse(resultSet.getBoolean(3)); } + } + } + + @Test + void setWrongParameter() throws SQLException { + setWrongParameter("SET foo=bar", Map.of(), "foo"); + } + @Test + void setCorrectThenWrongParameter() throws SQLException { + setWrongParameter("SET time_zone = 'EST';SET bar=tar", Map.of("time_zone", "EST"), "bar"); + } + + /** + * Connect to DB using {@code advanced_mode} sent in JDBC URL and set {@code force_pgdate_timestampntz} that requires advanced mode. + * @throws SQLException if connection fails + */ + @Test + void successfulSettingOfPropertyThatRequiresAdvancedModeConfiguredWhenConnectionIsCreated() throws SQLException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + String url = current.toJdbcUrl() + "&advanced_mode=1"; + try (Connection connection = DriverManager.getConnection(url, current.getPrincipal(), current.getSecret())) { + setParam(connection, "force_pgdate_timestampntz", "1"); + } + } + + /** + * Connect to DB without {@code advanced_mode}. Then set {@code advanced_mode=1} and {@code force_pgdate_timestampntz} that requires advanced mode. + * @throws SQLException if connection fails + */ + @Test + void successfulSettingOfPropertyThatRequiresAdvancedModePreviouslySetAtRuntime() throws SQLException { + try (Connection connection = createConnection()) { + setParam(connection, "advanced_mode", "1"); + setParam(connection, "force_pgdate_timestampntz", "1"); + } + } + + /** + * Try to set {@code force_pgdate_timestampntz} that requires advanced mode that was not set. This test will fail. + * @throws SQLException if connection fails + */ + @Test + void failedSettingPropertyThatRequiresAdvancedModeThatWasNotSet() throws SQLException { + try (Connection connection = createConnection()) { + assertFailingSet(connection, "force_pgdate_timestampntz"); + } + } + + /** + * Connect to DB using {@code advanced_mode} sent in JDBC URL. Then set {@code advanced_mode=0} and + * try to set {@code force_pgdate_timestampntz} that requires advanced mode and therefore fails. + * @throws SQLException if connection fails + */ + @Test + void failedSettingPropertyThatRequiresAdvancedModeThatWasUnset() throws SQLException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + String url = current.toJdbcUrl() + "&advanced_mode=1"; + try (Connection connection = DriverManager.getConnection(url, current.getPrincipal(), current.getSecret())) { + setParam(connection, "advanced_mode", "0"); + assertFailingSet(connection, "force_pgdate_timestampntz"); + } + } + + private void assertFailingSet(Connection connection, String paramName) { + FireboltException e = assertThrows(FireboltException.class, () -> setParam(connection, paramName, "1")); + assertTrue(e.getMessage().contains(paramName) && e.getMessage().contains("not allowed"), format("error message say that parameter %s is not allowed but was %s", paramName, e.getMessage())); + } + + private void setWrongParameter(String set, Map expectedAdditionalProperties, String expectedWrongPropertyName) throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + String message = assertThrows(SQLException.class, () -> statement.execute(set)).getMessage(); + String expected1 = format("parameter %s is not allowed", expectedWrongPropertyName); + String expected2 = format("query param not allowed: %s", expectedWrongPropertyName); + assertTrue(message.contains(expected1) || message.contains(expected2), + format("Unexpected error message: '%s'. Message should contain statement: '%s' or '%s'", message, expected1, expected2)); + assertEquals(expectedAdditionalProperties, ((FireboltConnection)connection).getSessionProperties().getAdditionalProperties()); } } @@ -191,18 +336,146 @@ void empty(String sql) throws SQLException { } @Test - void specialIntValue() throws SQLException { - try (Connection connection = createConnection()) { - try (Statement statement = connection.createStatement()) { - ResultSet resultSet = statement.executeQuery("SELECT 1/0"); - resultSet.next(); - assertThrows(IllegalArgumentException.class, () -> resultSet.getShort(1)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getInt(1)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getLong(1)); - assertEquals(Float.POSITIVE_INFINITY, resultSet.getFloat(1)); - assertEquals(Double.POSITIVE_INFINITY, resultSet.getDouble(1)); + @Tag("v1") + void divisionByZero() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + assertThrows(SQLException.class, () -> statement.executeQuery("SELECT 1/0")); + } + } + + /** + * This test validates that null values are sorted last. + * @throws SQLException if something is going wrong + * see com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#nullSorting + */ + @Test + void nullSortOrder() throws SQLException { + try (Connection connection = createConnection(); + Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("select x from (select null as x union all select '' as x union all select 'a' as x) order by x")) { + List actuals = new ArrayList<>(); + while(rs.next()) { + actuals.add(rs.getString(1)); + } + assertEquals(Arrays.asList("", "a", null), actuals); + } + } + + /** + * This test proves that unquoted query columns become lower case and quoted columns preserve case + * @throws SQLException if something is going wrong + * see com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#identifiersCase + * see com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#quotedIdentifiersCase + */ + @ParameterizedTest + @CsvSource(value = { + "select 1 as lower, 2 as UPPER, 3 AS MiXeD;lower,upper,mixed", + "select 1 as \"lower\", 2 as \"UPPER\", 3 AS \"MiXeD\";lower,UPPER,MiXeD" + }, delimiter = ';') + void mixedCaseSelect(String query, String expectedColumns) throws SQLException { + try (Connection connection = createConnection(); + Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery(query)) { + ResultSetMetaData md = rs.getMetaData(); + int n = md.getColumnCount(); + List names = new ArrayList<>(); + for (int i = 1; i <= n; i++) { + names.add(md.getColumnName(i)); + } + assertEquals(Arrays.asList(expectedColumns.split(",")), names); + } + } + + /** + * This test proves that unquoted table name is stored in lower case and quoted in mixed case + * @throws SQLException if something is going wrong + * see com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#identifiersCase + * see com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#quotedIdentifiersCase + */ + @ParameterizedTest + @CsvSource(value = { + "CREATE FACT TABLE Case_Test (x long);case_test;Case_Test;case_test", + "CREATE FACT TABLE \"Case_Test\" (x long);Case_Test;case_test;\"Case_Test\"" + }, delimiter = ';') + void mixedCaseTable(String createTable, String expectedTableName, String unexpectedTableName, String dropTableName) throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + try { + statement.executeUpdate(createTable); + try (ResultSet rs = statement.executeQuery(format("select table_name from information_schema.tables where table_name = '%s'", expectedTableName))) { + assertTrue(rs.next()); + assertEquals(expectedTableName, rs.getString(1)); + assertFalse(rs.next()); + } + try (ResultSet rs = statement.executeQuery(format("select table_name from information_schema.tables where table_name = '%s'", unexpectedTableName))) { + assertFalse(rs.next()); + } + } finally { + statement.executeUpdate("DROP TABLE IF EXISTS " + dropTableName); } } } + /** + * Validates that specific statement fails because used function is not supported. If specific test fails, i.e. + * the query succeeds this means that used function is supported now. In this case corresponding unit test should be + * fixed too. + * @param query the SQL statement that should fail + * @throws SQLException if something is going wrong + */ + @ParameterizedTest + @ValueSource(strings = { + "SELECT CONVERT(varchar, 3.14)" //com.firebolt.jdbc.metadata.FireboltDatabaseMetadataTest#supportsConvert + }) + void failingQuery(String query) throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + assertThrows(SQLException.class, () -> statement.execute(query)); + } + } + + @Test + void caseInsensitiveGetter() throws SQLException { + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery("select table_schema, table_name as NAME from information_schema.tables")) { + while(rs.next()) { + assertEquals(rs.getString("table_schema"), rs.getString("TABLE_SCHEMA")); + assertEquals(rs.getString("table_schema"), rs.getString("Table_Schema")); + assertEquals(rs.getString("table_schema"), rs.getString("TaBlE_ScHeMa")); + assertEquals(rs.getString("name"), rs.getString("NAME")); + assertEquals(rs.getString("name"), rs.getString("NaMe")); + } + + } + } + + @ParameterizedTest + @ValueSource(ints = {1, 3, 5, 50}) + void maxFieldSize(int maxFieldSize) throws SQLException { + String query = "select table_name from information_schema.tables order by table_name"; + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.setMaxFieldSize(maxFieldSize); + readValues(statement, query, 1).forEach(table -> assertThat(table.length(), Matchers.lessThanOrEqualTo(maxFieldSize))); + } + } + + @ParameterizedTest + @ValueSource(ints = {0, -1, 100}) + void unlimitedMaxFieldSize(int maxFieldSize) throws SQLException { + String query = "select table_name from information_schema.tables order by table_name"; + try (Connection connection = createConnection(); + Statement unlimitedStatement = connection.createStatement(); + Statement limitedStatement = connection.createStatement()) { + limitedStatement.setMaxFieldSize(maxFieldSize); + assertEquals(readValues(unlimitedStatement, query, 1), readValues(limitedStatement, query, 1)); + } + } + + private Collection readValues(Statement statement, String query, int columnIndex) throws SQLException { + List values = new ArrayList<>(); + try (ResultSet rs = statement.executeQuery(query)) { + while(rs.next()) { + values.add(rs.getString(columnIndex)); + } + } + return values; + } } diff --git a/src/integrationTest/java/integration/tests/SystemEngineDatabaseMetaDataTest.java b/src/integrationTest/java/integration/tests/SystemEngineDatabaseMetaDataTest.java new file mode 100644 index 00000000..92751b57 --- /dev/null +++ b/src/integrationTest/java/integration/tests/SystemEngineDatabaseMetaDataTest.java @@ -0,0 +1,221 @@ +package integration.tests; + +import com.firebolt.jdbc.CheckedFunction; +import integration.IntegrationTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.stream.IntStream; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SystemEngineDatabaseMetaDataTest extends IntegrationTest { + private Connection connection; + private DatabaseMetaData dbmd; + + @BeforeAll + void connect() throws SQLException { + connection = createConnection(getSystemEngineName()); + dbmd = connection.getMetaData(); + } + + @AfterAll + void disconnect() throws SQLException { + connection.close(); + } + + @Test + void readOnly() throws SQLException { + assertFalse(connection.isReadOnly()); + assertFalse(connection.getMetaData().isReadOnly()); + } + + @Test + @Tag("v2") + void getSchemas() throws SQLException { + String database = integration.ConnectionInfo.getInstance().getDatabase(); + assertEquals(List.of(List.of("information_schema", database)), getSchemas(DatabaseMetaData::getSchemas)); + } + + @ParameterizedTest + @CsvSource(value = { + ",", + ",information_schema", + ",information%", + ",%schema", + ",%form%", + "{database},", + "{database},information_schema", + "{database},information%", + "{database},%schema", + "{database},%form%", + }) + @Tag("v2") + void getSchemasInformationSchema(String catalog, String schemaPattern) throws SQLException { + String database = integration.ConnectionInfo.getInstance().getDatabase(); + String cat = catalog == null ? null : catalog.replace("{database}", database); + assertEquals(List.of(List.of("information_schema", database)), getSchemas(dbmd -> dbmd.getSchemas(cat, schemaPattern))); + } + + @ParameterizedTest + @CsvSource(value = { + ",,,,tables,", + ",information_schema,,,tables,", + ",information%,,,tables,", + ",%schema,,,tables,", + ",%form%,,,tables,", + "{database},,,,tables,", + "{database},information_schema,,,tables,", + "{database},information%,,,tables,", + "{database},%schema,,,tables,", + "{database},%form%,,,tables,", + + ",,%in%,,engines,tables", + ",information_schema,%in%,,engines,tables", + ",information%,%in%,,engines,tables", + ",%schema,%in%,,engines,tables", + ",%form%,%in%,,engines,tables", + "{database},,%in%,,engines,tables", + "{database},information_schema,%in%,,engines,tables", + "{database},information%,%in%,,engines,tables", + "{database},%schema,%in%,,engines,tables", + "{database},%form%,%in%,,engines,tables", + + ",,%in%,VIEW,engines,tables", + ",information_schema,%in%,VIEW,engines,tables", + ",information%,%in%,VIEW,engines,tables", + ",%schema,%in%,VIEW,engines,tables", + ",%form%,%in%,VIEW,engines,tables", + "{database},,%in%,VIEW,engines,tables", + "{database},information_schema,%in%,VIEW,engines,tables", + "{database},information%,%in%,VIEW,engines,tables", + "{database},%schema,%in%,VIEW,engines,tables", + "{database},%form%,%in%,VIEW,engines,tables", + + ",,,VIEW;TABLE,engines,", + ",,%in%,VIEW;TABLE,engines,tables", + ",,,TABLE,,", + }) + @Tag("v2") + void getTables(String catalog, String schemaPattern, String tableNamePattern, String types, String requiredTableName, String forbiddenTableName) throws SQLException { + String database = integration.ConnectionInfo.getInstance().getDatabase(); + String requiredCatalog = catalog == null ? null : catalog.replace("{database}", database); + String[] requiredTypes = types == null ? null : types.split(";"); + List> rows = readResultSet(dbmd.getTables(requiredCatalog, schemaPattern, tableNamePattern, requiredTypes)); + Collection tables = new HashSet<>(); + for (List row : rows) { + assertEquals(database, row.get(0)); + assertEquals("information_schema", row.get(1)); + assertFalse(((String) row.get(2)).isEmpty()); + assertEquals("VIEW", row.get(3)); + tables.add((String) row.get(2)); + System.out.println(row.get(2)); + } + if (requiredTableName == null) { + assertTrue(tables.isEmpty()); + } else { + assertTrue(tables.contains(requiredTableName), format("Required table %s is not found", requiredTableName)); + } + if (forbiddenTableName != null) { + assertFalse(tables.contains(forbiddenTableName), format("Forbidden table %s is found", forbiddenTableName)); + } + } + + @ParameterizedTest + @CsvSource(value = { + ",,,,information_schema.columns.column_name,", + ",information_schema,,,information_schema.columns.column_name,", + ",information%,,,information_schema.columns.column_name,", + ",%schema,,,information_schema.columns.column_name,", + ",%form%,,,information_schema.columns.column_name,", + "{database},,,,information_schema.columns.column_name,", + "{database},information_schema,,,information_schema.columns.column_name,", + "{database},information%,,,information_schema.columns.column_name,", + "{database},%schema,,,information_schema.columns.column_name,", + "{database},%form%,,,information_schema.columns.column_name,", + + ",,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + ",information_schema,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + ",information%,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + ",%schema,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + ",%form%,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + "{database},,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + "{database},information_schema,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + "{database},information%,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + "{database},%schema,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + "{database},%form%,%in%,,information_schema.engines.engine_name,information_schema.tables.table_name", + + ",,%in%,created,information_schema.engines.created,information_schema.engines.engine_name", + ",information_schema,%in%,last_altered,information_schema.engines.last_altered,information_schema.engines.engine_name", + ",information%,%in%,description,information_schema.engines.description,information_schema.engines.engine_name", + ",%schema,%in%,table_name,information_schema.indexes.table_name,information_schema.indexes.table_type", + ",%form%,%in%,table_schema,information_schema.indexes.table_schema,information_schema.indexes.table_name", + "{database},,%in%,table_catalog,information_schema.indexes.table_catalog,information_schema.indexes.table_schema", + "{database},information_schema,%ac%,account_id,information_schema.accounts.account_id,information_schema.accounts.region", + "{database},information%,%in%,engine_name,information_schema.engines.engine_name,information_schema.engines.description", + "{database},%schema,%in%,region,information_schema.engines.region,information_schema.engines.status", + "{database},%form%,%in%,type,information_schema.engines.type,information_schema.engines.status", + + ",,,nobody,,information_schema.columns.column_name", + ",,%in%,no-one,,information_schema.columns.column_name", + ",,,does-not-exist,,information_schema.columns.column_name", + }) + @Tag("v2") + void getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern, String requiredColumn, String forbiddenColumn) throws SQLException { + String database = integration.ConnectionInfo.getInstance().getDatabase(); + String requiredCatalog = catalog == null ? null : catalog.replace("{database}", database); + List> rows = readResultSet(dbmd.getColumns(requiredCatalog, schemaPattern, tableNamePattern, columnNamePattern)); + Collection columns = new HashSet<>(); + for (List row : rows) { + assertEquals(database, row.get(0)); + assertEquals("information_schema", row.get(1)); + assertFalse(((String) row.get(2)).isEmpty()); + assertFalse(((String) row.get(3)).isEmpty()); + columns.add(IntStream.of(1, 2, 3).boxed().map(i -> (String)row.get(i)).collect(joining("."))); + } + if (requiredColumn == null) { + assertTrue(columns.isEmpty()); + } else { + assertTrue(columns.contains(requiredColumn), format("Required column %s is not found", requiredColumn)); + } + if (forbiddenColumn != null) { + assertFalse(columns.contains(forbiddenColumn), format("Forbidden column %s is found", forbiddenColumn)); + } + } + + private List> getSchemas(CheckedFunction schemasGetter) throws SQLException { + return readResultSet(schemasGetter.apply(dbmd)); + } + + private List> readResultSet(ResultSet rs) throws SQLException { + List> rows = new ArrayList<>(); + ResultSetMetaData rsmd = rs.getMetaData(); + int n = rsmd.getColumnCount(); + + while (rs.next()) { + List row = new ArrayList<>(); + rows.add(row); + for (int i = 1; i <= n; i++) { + row.add(rs.getObject(i)); + } + } + return rows; + } +} diff --git a/src/integrationTest/java/integration/tests/SystemEngineTest.java b/src/integrationTest/java/integration/tests/SystemEngineTest.java index 80870ead..7b40c729 100644 --- a/src/integrationTest/java/integration/tests/SystemEngineTest.java +++ b/src/integrationTest/java/integration/tests/SystemEngineTest.java @@ -1,58 +1,360 @@ package integration.tests; +import com.firebolt.jdbc.connection.CacheListener; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import integration.ConnectionInfo; +import integration.EnvironmentCondition; +import integration.IntegrationTest; +import org.junit.Assert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; -import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; -import org.junit.jupiter.api.*; +import static com.firebolt.jdbc.connection.FireboltConnectionUserPassword.SYSTEM_ENGINE_NAME; +import static integration.EnvironmentCondition.Attribute.fireboltVersion; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Map.entry; +import static java.util.stream.Collectors.toList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; -import integration.IntegrationTest; -import lombok.CustomLog; - -@CustomLog @TestMethodOrder(MethodOrderer.OrderAnnotation.class) public class SystemEngineTest extends IntegrationTest { - private static final String DATABASE_NAME = "jdbc_system_engine_integration_test"; - private static final String ENGINE_NAME = "jdbc_system_engine_integration_test_engine"; - private static final String ENGINE_NEW_NAME = "jdbc_system_engine_integration_test_engine_2"; - private static final String SYSTEM_ENGINE_NAME = "system"; + private static final long ID = ProcessHandle.current().pid() + System.currentTimeMillis(); + private static final String SECOND_DATABASE_NAME = "jdbc_system_engine_integration_test_" + ID; + private static final String ENGINE_NAME = SECOND_DATABASE_NAME + "_engine"; + private static final String ENGINE_NEW_NAME = ENGINE_NAME + "_2"; + + private static final String USE_DATABASE_NAME = "jdbc_use_db_" + ID; + private static final String TABLE = USE_DATABASE_NAME + "_table"; + private static final String TABLE1 = TABLE + "_1"; + private static final String TABLE2 = TABLE + "_2"; + + private static final Logger log = Logger.getLogger(SystemEngineTest.class.getName()); @BeforeAll void beforeAll() { try { - executeStatementFromFile("/statements/system/ddl.sql", SYSTEM_ENGINE_NAME); + executeStatementFromFile("/statements/system/ddl.sql", getSystemEngineName()); } catch (Exception e) { - log.warn("Could not execute statement", e); + log.log(Level.WARNING, "Could not execute statement", e); } } @AfterAll void afterAll() { try { - executeStatementFromFile("/statements/system/cleanup.sql", SYSTEM_ENGINE_NAME); + executeStatementFromFile("/statements/system/cleanup.sql", getSystemEngineName()); } catch (Exception e) { - log.warn("Could not execute statement", e); + log.log(Level.WARNING, "Could not execute statement", e); + } + } + + @Test + void shouldSelect1() throws SQLException { + try (Connection connection = createConnection(""); + ResultSet rs = connection.createStatement().executeQuery("SELECT 1")) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt(1)); + assertFalse(rs.next()); + } + } + + @Test + void shouldThrowExceptionWhenExecutingWrongQuery() throws SQLException { + try (Connection connection = createConnection(getSystemEngineName()); Statement statement = connection.createStatement()) { + String errorMessage = assertThrows(FireboltException.class, () -> statement.executeQuery("select wrong query")).getMessage(); + assertTrue(errorMessage.contains("Column 'wrong' does not exist.")); + } + } + + @Test + @EnvironmentCondition(value = "4.2.0", attribute = fireboltVersion, comparison = EnvironmentCondition.Comparison.GE) + void shouldThrowExceptionWhenExecutingWrongQueryWithJsonError() throws SQLException { + try (Connection connection = createConnection(getSystemEngineName()); Statement statement = connection.createStatement()) { + statement.execute("set advanced_mode=1"); + statement.execute("set enable_json_error_output_format=true"); + String errorMessage = assertThrows(FireboltException.class, () -> statement.executeQuery("select wrong query")).getMessage(); + assertTrue(errorMessage.contains("Column 'wrong' does not exist.")); + } + } + + @Test + void shouldFailToSelectFromCustomDbUsingSystemEngine() throws SQLException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + String systemEngineJdbcUrl = new ConnectionInfo(current.getPrincipal(), current.getSecret(), + current.getEnv(), current.getDatabase(), current.getAccount(), getSystemEngineName(), + current.getApi()).toJdbcUrl(); + String customEngineJdbcUrl = current.toJdbcUrl(); + String principal = current.getPrincipal(); + String secret = current.getSecret(); + Collection expectedErrorMessages = Set.of( + "Queries against table dummy require a user engine", + "The system engine doesn't support queries against table dummy. Run this query on a user engine.", + "relation \"dummy\" does not exist"); + + try (Connection systemConnection = DriverManager.getConnection(systemEngineJdbcUrl, principal, secret); + Connection customConnection = DriverManager.getConnection(customEngineJdbcUrl, principal, secret)) { + + try { + customConnection.createStatement().executeUpdate("CREATE DIMENSION TABLE dummy(id INT)"); + try (ResultSet rs = customConnection.createStatement().executeQuery("select count(*) from dummy")) { + assertTrue(rs.next()); + assertEquals(0, rs.getInt(1)); + assertFalse(rs.next()); + } + FireboltException e = assertThrows(FireboltException.class, () -> systemConnection.createStatement().executeQuery("select count(*) from dummy")); + String actualErrorMessage = e.getErrorMessageFromServer().replaceAll("\r?\n", ""); + assertTrue(expectedErrorMessages.contains(actualErrorMessage), "Unexpected error message: " + actualErrorMessage); + } finally { + try { + customConnection.createStatement().executeUpdate("DROP TABLE dummy"); + } catch(SQLException e) { + // ignore the exception here; even if it happens it does not matter. + } + + } + } + } + + @ParameterizedTest + @ValueSource(strings = {"", "DATABASE"}) + @Tag("v2") + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void useDatabase(String entityType) throws SQLException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + try (Connection connection = createConnection(getSystemEngineName())) { + try { + connection.createStatement().executeUpdate(format("USE %s \"%s\"", entityType, current.getDatabase())); // use current DB; shouldn't have any effect + assertNull(getTableDbName(connection, TABLE1)); // the table does not exist yet + connection.createStatement().executeUpdate(format("CREATE TABLE \"%s\" ( id LONG)", TABLE1)); // create table1 in current DB + assertEquals(current.getDatabase(), getTableDbName(connection, TABLE1)); // now table t1 exists + Assert.assertThrows(SQLException.class, () -> connection.createStatement().executeUpdate(format("USE %s %s", entityType, USE_DATABASE_NAME))); // DB does not exist + connection.createStatement().executeUpdate(format("CREATE DATABASE IF NOT EXISTS \"%s\"", USE_DATABASE_NAME)); // create DB + connection.createStatement().executeUpdate(format("USE %s %s", entityType, USE_DATABASE_NAME)); // Now this should succeed + connection.createStatement().executeUpdate(format("CREATE TABLE \"%s\" ( id LONG)", TABLE2)); // create table2 in other DB + assertNull(getTableDbName(connection, TABLE1)); // table1 does not exist here + assertEquals(USE_DATABASE_NAME, getTableDbName(connection, TABLE2)); // but table2 does exist + } finally { + // now clean up everything + for (String query : new String[] { + format("USE %s \"%s\"", entityType, USE_DATABASE_NAME), // switch to DB that should be current just in case because the previous code can fail at any phase + format("DROP TABLE %s", TABLE2), + format("DROP DATABASE \"%s\"", USE_DATABASE_NAME), + format("USE %s \"%s\"", entityType, current.getDatabase()), // now switch back + format("DROP TABLE %s", TABLE1)}) { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate(query); + } catch (SQLException e) { // catch just in case to do our best to clean everything even if test has failed + log.log(Level.WARNING, "Cannot perform query " + query, e); + } + } + } } } @Test - void shouldExecuteQueriesUsingSystemEngine() throws SQLException { - try (Connection connection = this.createConnection(SYSTEM_ENGINE_NAME)) { - List queries = Arrays.asList(String.format("CREATE DATABASE %s", DATABASE_NAME), - String.format("CREATE ENGINE %s", ENGINE_NAME), - String.format("ATTACH ENGINE %s TO %s;", ENGINE_NAME, DATABASE_NAME), - String.format("ALTER DATABASE %s WITH DESCRIPTION = 'JDBC Integration test'", DATABASE_NAME), - String.format("ALTER ENGINE %s RENAME TO %s", ENGINE_NAME, ENGINE_NEW_NAME), - String.format("START ENGINE %s", ENGINE_NEW_NAME), String.format("STOP ENGINE %s", ENGINE_NEW_NAME), - String.format("DROP ENGINE %s", ENGINE_NEW_NAME), String.format("DROP DATABASE %s", DATABASE_NAME)); - for (String query : queries) { - try (Statement statement = connection.createStatement()) { - statement.execute(query); + @Tag("v2") + @Tag("slow") + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void useEngine() throws SQLException { + try (Connection connection = createConnection(getSystemEngineName())) { + try { + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + assertThrows(SQLException.class, () -> connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", ENGINE_NAME))); + connection.createStatement().executeUpdate(format("CREATE ENGINE \"%s\"", ENGINE_NAME)); + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", ENGINE_NAME)); + connection.createStatement().executeUpdate(format("CREATE DATABASE IF NOT EXISTS \"%s\"", USE_DATABASE_NAME)); + connection.createStatement().executeUpdate(format("USE DATABASE \"%s\"", USE_DATABASE_NAME)); + connection.createStatement().executeUpdate(format("CREATE TABLE \"%s\" ( id LONG)", TABLE1)); + connection.createStatement().executeUpdate(format("INSERT INTO %s (id) VALUES (1)", TABLE1)); // should succeed using user engine + // switch back to the system engine + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + assertThrows(SQLException.class, () -> connection.createStatement().executeUpdate(format("INSERT INTO %s (id) VALUES (1)", TABLE1))); // system engine cannot insert data + } finally { + connection.createStatement().executeUpdate(format("USE DATABASE \"%s\"", USE_DATABASE_NAME)); + connection.createStatement().executeUpdate(format("DROP TABLE %s", TABLE1)); + connection.createStatement().executeUpdate(format("DROP DATABASE \"%s\"", USE_DATABASE_NAME)); + connection.createStatement().executeUpdate(format("STOP ENGINE \"%s\"", ENGINE_NAME)); + connection.createStatement().executeUpdate(format("DROP ENGINE \"%s\"", ENGINE_NAME)); + } + } + } + + @Test + @Tag("v2") + @Tag("slow") + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void useEngineMixedCase() throws SQLException { + String mixedCaseEngineName = "JavaIntegrationTestMixedCase" + ID; + try (Connection connection = createConnection(getSystemEngineName())) { + try { + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + connection.createStatement().executeUpdate(format("CREATE ENGINE \"%s\"", mixedCaseEngineName)); + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", mixedCaseEngineName)); + assertThrows(SQLException.class, () -> connection.createStatement().executeUpdate(format("USE ENGINE %s", mixedCaseEngineName))); + } finally { + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + connection.createStatement().executeUpdate(format("STOP ENGINE \"%s\"", mixedCaseEngineName)); + connection.createStatement().executeUpdate(format("DROP ENGINE \"%s\"", mixedCaseEngineName)); + } + } + } + + @Test + @Tag("v2") + @Tag("slow") + @EnvironmentCondition(value = "2", comparison = EnvironmentCondition.Comparison.GE) + void useEngineMixedCaseToLowerCase() throws SQLException { + String mixedCaseEngineName = "JavaIntegrationTestToLowerCase" + ID; + try (Connection connection = createConnection(getSystemEngineName())) { + try { + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + // engine name is lower cased because it is not quoted + connection.createStatement().executeUpdate(format("CREATE ENGINE %s", mixedCaseEngineName)); + connection.createStatement().executeUpdate(format("USE ENGINE %s", mixedCaseEngineName)); + // engine name remains mixed case and statement fails because engine name was not quoted when we created the engine + assertThrows(SQLException.class, () -> connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", mixedCaseEngineName))); + } finally { + connection.createStatement().executeUpdate(format("USE ENGINE \"%s\"", SYSTEM_ENGINE_NAME)); + connection.createStatement().executeUpdate(format("STOP ENGINE %s", mixedCaseEngineName)); + connection.createStatement().executeUpdate(format("DROP ENGINE %s", mixedCaseEngineName)); + } + } + } + + @Test + @Tag("v2") + void connectToAccountWithoutUser() throws SQLException, IOException { + ConnectionInfo current = integration.ConnectionInfo.getInstance(); + String database = current.getDatabase(); + String serviceAccountName = format("%s_%d_sa_no_user", database, System.currentTimeMillis()); + try (Connection connection = createConnection(getSystemEngineName())) { + try { + connection.createStatement().executeUpdate(format("CREATE SERVICE ACCOUNT \"%s\" WITH DESCRIPTION = 'Ecosytem test with no user'", serviceAccountName)); + // This what I want to do here +// ResultSet genKeyRs = connection.createStatement().executeQuery(format("CALL fb_GENERATESERVICEACCOUNTKEY('%s')", serviceAccountName)); +// assertTrue(genKeyRs.next()); +// String clientId = genKeyRs.getString(2); +// String clientSecret = genKeyRs.getString(3); + // But response of this command is incorrect (FIR-28997), so we have to retrieve clientId and clientSecret using SELECT + String clientSecret = getClientSecret(connection, serviceAccountName, current.getDatabase()); + // end of patch against FIR-28997 +// if (clientId == null || clientId.isEmpty()) { // Currently this is bugged so retrieve id via a query. FIR-28719 + ResultSet serviceAccountRs = connection.createStatement().executeQuery(format("SELECT service_account_id FROM information_schema.service_accounts WHERE service_account_name='%s'", serviceAccountName)); + assertTrue(serviceAccountRs.next()); + String clientId = serviceAccountRs.getString(1); +// } + String jdbcUrl = format("jdbc:firebolt:%s?env=%s&account=%s&engine=%s", database, current.getEnv(), current.getAccount(), current.getEngine()); + + ((CacheListener)connection).cleanup(); + SQLException e = assertThrows(SQLException.class, () -> DriverManager.getConnection(jdbcUrl, clientId, clientSecret)); + assertTrue(e.getMessage().matches(format("Account '%s' does not exist in this organization or is not authorized.+RBAC.+", current.getAccount())), "Unexpected exception message: " + e.getMessage()); + } finally { + connection.createStatement().executeUpdate(format("DROP SERVICE ACCOUNT \"%s\"", serviceAccountName)); + } + } + } + + // This method should be removed when FIR-28997 is fixed + private String getClientSecret(Connection connection, String serviceAccountName, String database) throws SQLException, IOException { + FireboltConnection fbConn = (FireboltConnection)connection; + String accessToken = fbConn.getAccessToken().orElseThrow(() -> new IllegalStateException("access token is not found")); + FireboltProperties fbProps = fbConn.getSessionProperties(); + URL url = new URL(format("%s/query?output_format=TabSeparatedWithNamesAndTypes&database=%s&account_id=%s", fbProps.getHttpConnectionUrl(), database, fbProps.getAccountId())); + HttpURLConnection con = (HttpURLConnection)url.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + con.setRequestProperty("authorization", "Bearer " + accessToken); + con.setDoOutput(true); + try (PrintStream ps = new PrintStream(con.getOutputStream())) { + ps.println(format("CALL fb_GENERATESERVICEACCOUNTKEY('%s')", serviceAccountName)); + } + BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream(), UTF_8)); + String[] data = br.readLine().split("\\t"); + String clientSecret = data[2]; + return clientSecret; + + } + + private String getTableDbName(Connection connection, String table) throws SQLException { + try (PreparedStatement ps = connection.prepareStatement("select table_catalog from information_schema.tables where table_name=?")) { + ps.setString(1, table); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString(1) : null; + } + } + } + + @Test + @Tag("slow") + @Tag("v2") // does not work on new V1 - DB cannot be managed by system engine + void shouldExecuteEngineManagementQueries() throws SQLException { + try (Connection connection = createConnection(getSystemEngineName())) { + try { + boolean attachEngineToDb = ((FireboltConnection)connection).getInfraVersion() < 2; + List queries = Stream.of( + entry(true, format("CREATE DATABASE IF NOT EXISTS \"%s\"", SECOND_DATABASE_NAME)), + entry(true, format("CREATE ENGINE \"%s\"", ENGINE_NAME)), + entry(attachEngineToDb, format("ATTACH ENGINE \"%s\" TO \"%s\";", ENGINE_NAME, SECOND_DATABASE_NAME)), + entry(true, format("ALTER DATABASE \"%s\" SET DESCRIPTION = 'JDBC Integration test'", SECOND_DATABASE_NAME)), + entry(true, format("ALTER ENGINE \"%s\" RENAME TO \"%s\"", ENGINE_NAME, ENGINE_NEW_NAME)), + entry(true, format("START ENGINE \"%s\"", ENGINE_NEW_NAME))) + .filter(Map.Entry::getKey).map(Map.Entry::getValue).collect(toList()); + executeAll(connection, queries); + } finally { + // now clean up everything + for (String query : new String[]{ + format("STOP ENGINE \"%s\"", ENGINE_NEW_NAME), + format("DROP ENGINE \"%s\"", ENGINE_NEW_NAME), + format("DROP DATABASE \"%s\"", SECOND_DATABASE_NAME)}) { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate(query); + } catch (SQLException e) { // catch just in case to do our best to clean everything even if test has failed + log.log(Level.WARNING, "Cannot perform query " + query, e); + } } } } } + + private void executeAll(Connection connection, Iterable queries) throws SQLException { + for (String query : queries) { + try (Statement statement = connection.createStatement()) { + statement.execute(query); + } + } + } } diff --git a/src/integrationTest/java/integration/tests/TimeoutTest.java b/src/integrationTest/java/integration/tests/TimeoutTest.java index 6fcca31c..ac3e14b2 100644 --- a/src/integrationTest/java/integration/tests/TimeoutTest.java +++ b/src/integrationTest/java/integration/tests/TimeoutTest.java @@ -1,32 +1,67 @@ package integration.tests; -import static org.junit.jupiter.api.Assertions.fail; +import com.firebolt.jdbc.connection.FireboltConnection; +import integration.EnvironmentCondition; +import integration.IntegrationTest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import java.sql.Connection; +import java.sql.SQLException; import java.sql.Statement; +import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -import integration.IntegrationTest; -import lombok.CustomLog; +import static integration.EnvironmentCondition.Attribute.databaseVersion; +import static integration.EnvironmentCondition.Comparison.GE; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertTrue; -@CustomLog class TimeoutTest extends IntegrationTest { + private static final int MIN_TIME_SECONDS = 350; + private static final Map SERIES_SIZE = Map.of(1, 80000000000L, 2, 180000000000L); + private static final Logger log = Logger.getLogger(TimeoutTest.class.getName()); + private long startTime; + + @BeforeEach + void before() { + startTime = System.nanoTime(); + } + + @AfterEach + void after() { + long endTime = System.nanoTime(); + long elapsedTimeSeconds = (endTime - startTime) / 1_000_000_000; + log.log(Level.INFO, "Time elapsed: {0} seconds", elapsedTimeSeconds); + assertTrue(elapsedTimeSeconds > MIN_TIME_SECONDS, format("Test is too short. It took %d but should take at least %d seconds", elapsedTimeSeconds, MIN_TIME_SECONDS)); + } + + @Test + @Timeout(value = 10, unit = TimeUnit.MINUTES) + @Tag("v1") // generate_series is supported on all available engine of v2 + @Tag("slow") + void shouldExecuteRequestWithoutTimeoutV1() throws SQLException { + shouldExecuteRequestWithoutTimeout(); + } @Test - @Timeout(value = 7, unit = TimeUnit.MINUTES) - void shouldExecuteRequestWithoutTimeout() { - long startTime = System.nanoTime(); - try (Connection con = this.createConnection(); Statement stmt = con.createStatement()) { - this.setParam(con, "use_standard_sql", "0"); - this.setParam(con, "advanced_mode", "1"); - stmt.executeQuery("SELECT sleepEachRow(1) from numbers(360)"); - } catch (Exception e) { - log.error("Error", e); - fail(); + @Timeout(value = 10, unit = TimeUnit.MINUTES) + @EnvironmentCondition(value = "3.33", attribute = databaseVersion, comparison = GE) // generate_series is supported starting from version 3.33 on v2 + @Tag("v2") + @Tag("slow") + void shouldExecuteRequestWithoutTimeoutV2() throws SQLException { + shouldExecuteRequestWithoutTimeout(); + } + + private void shouldExecuteRequestWithoutTimeout() throws SQLException { + try (Connection con = createConnection(); Statement stmt = con.createStatement()) { + int infraVersion = ((FireboltConnection)con).getInfraVersion(); + stmt.executeQuery(format("SELECT (max(x) - min(x))/count(x) + avg(x) FROM generate_series(1,%d) r(x)", SERIES_SIZE.get(infraVersion))); } - log.info("Time elapsed: " + (System.nanoTime() - startTime) / 1_000_000_000 + " seconds"); } } diff --git a/src/integrationTest/java/integration/tests/TimestampTest.java b/src/integrationTest/java/integration/tests/TimestampTest.java index 5cd77f73..99732ea0 100644 --- a/src/integrationTest/java/integration/tests/TimestampTest.java +++ b/src/integrationTest/java/integration/tests/TimestampTest.java @@ -3,7 +3,6 @@ import com.firebolt.jdbc.testutils.AssertionUtil; import integration.IntegrationTest; import io.zonky.test.db.postgres.embedded.EmbeddedPostgres; -import lombok.CustomLog; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -37,7 +36,6 @@ import static java.sql.Types.TIMESTAMP_WITH_TIMEZONE; import static org.junit.jupiter.api.Assertions.assertEquals; -@CustomLog @DefaultTimeZone("UTC") class TimestampTest extends IntegrationTest { private static final TimeZone UTC_TZ = TimeZone.getTimeZone("UTC"); @@ -58,9 +56,9 @@ void tearDown() throws IOException { @Test void shouldGetTimeObjectsInDefaultUTCTimezone() throws SQLException { - try (Connection connection = this.createConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement.executeQuery("SELECT TO_TIMESTAMP('1975/01/01 23:01:01', 'yyyy/MM/DD hh24:mi:ss');")) { + try (Connection connection = createConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT TO_TIMESTAMP('1975/01/01 23:01:01', 'yyyy/MM/DD hh24:mi:ss');")) { resultSet.next(); ZonedDateTime zonedDateTime = ZonedDateTime.of(1975, 1, 1, 23, 1, 1, 0, TimeZone.getTimeZone("UTC").toZoneId()); @@ -80,10 +78,9 @@ void shouldGetTimeObjectsInDefaultUTCTimezone() throws SQLException { @Test void shouldGetParsedTimeStampExtTimeObjects() throws SQLException { - try (Connection connection = this.createConnection(); - Statement statement = connection.createStatement(); - ResultSet resultSet = statement - .executeQuery("SELECT CAST('1111-11-11 ' || '12:00:03' AS timestamptz);")) { + try (Connection connection = createConnection(); + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery("SELECT CAST('1111-11-11 ' || '12:00:03' AS timestamptz);")) { resultSet.next(); ZonedDateTime expectedTimestampZdt = ZonedDateTime.of(1111, 11, 11, 12, 0, 3, 0, TimeZone.getTimeZone("UTC").toZoneId()); @@ -110,7 +107,7 @@ void shouldRemoveOffsetDIffWhenTimestampOffsetHasChanged() throws SQLException { // Asia/Kolkata had an offset of +05:21:10 in 1899 vs +05:30 today. The // timestamp returned should have the time 00:00:00 (so without the difference // of 08:50). - try (Connection connection = this.createConnection(); + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT CAST('1899-01-01 00:00:00' AS timestampntz);")) { resultSet.next(); @@ -133,7 +130,7 @@ void shouldRemoveOffsetDIffWhenTimestampOffsetHasChangedCET() throws SQLExceptio // Asia/Kolkata had an offset of +05:21:10 in 1899 vs +05:30 today. The // timestamp returned should have the time 00:00:00 (so without the difference // of 08:50). - try (Connection connection = this.createConnection(); + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT CAST('2100-05-11 00:00:00' AS timestamp);")) { resultSet.next(); @@ -143,7 +140,7 @@ void shouldRemoveOffsetDIffWhenTimestampOffsetHasChangedCET() throws SQLExceptio @Test void shouldReturnTimestampFromTimestampntz() throws SQLException { - try (Connection connection = this.createConnection(); + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT '2022-05-10 23:01:02.123'::timestampntz;")) { resultSet.next(); @@ -165,13 +162,13 @@ void shouldReturnTimestampFromTimestampntz() throws SQLException { @Test void shouldReturnTimestampFromTimestamptz() throws SQLException { - try (Connection connection = this.createConnection(); + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); Statement statementWithTzInQuery = connection.createStatement(); ResultSet resultSetWithTzInQuery = statementWithTzInQuery .executeQuery("SELECT '1975-05-10 23:01:02.123 EST'::timestamptz;")) { // Same as: SELECT '1975-05-10 23:01:02.123 Europe/Berlin'::timestamptz; - statement.execute("SET advanced_mode=1;SET time_zone = 'EST';"); + statement.execute("SET time_zone = 'EST';"); ResultSet resultSetWithTzAsQueryParam = statement .executeQuery("SELECT '1975-05-10 23:01:02.123'::timestamptz;"); resultSetWithTzInQuery.next(); @@ -202,8 +199,8 @@ void shouldReturnTimestampFromTimestamptz() throws SQLException { @Test void shouldReturnTimestampFromTimestampTzWithTzWithHoursAndMinutes() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { - statement.execute("SET advanced_mode=1;SET time_zone = 'Asia/Calcutta';"); // The server will return a tz in + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.execute("SET time_zone = 'Asia/Calcutta';"); // The server will return a tz in // the format +05:30 ResultSet resultSet = statement.executeQuery("SELECT '1975-05-10 23:01:02.123'::timestamptz;"); resultSet.next(); @@ -214,8 +211,8 @@ void shouldReturnTimestampFromTimestampTzWithTzWithHoursAndMinutes() throws SQLE @Test void shouldReturnTimestampFromTimestampTzWithTzWithHoursAndMinutesAndSeconds() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { - statement.execute("SET advanced_mode=1;SET time_zone = 'Asia/Calcutta';"); // The server will return a tz in + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.execute("SET time_zone = 'Asia/Calcutta';"); // The server will return a tz in // the format +05:30 ResultSet resultSet = statement.executeQuery("SELECT '1111-01-05 17:04:42.123456'::timestamptz"); resultSet.next(); @@ -231,8 +228,8 @@ void shouldReturnTimestampFromTimestampTzWithTzWithHoursAndMinutesAndSeconds() t @Test void shouldReturnTimestampFromDate() throws SQLException { - try (Connection connection = this.createConnection(); Statement statement = connection.createStatement()) { - statement.execute("SET advanced_mode=1; SET time_zone='Europe/Berlin';"); + try (Connection connection = createConnection(); Statement statement = connection.createStatement()) { + statement.execute("SET time_zone='Europe/Berlin';"); ResultSet resultSet = statement.executeQuery("SELECT '2022-05-10'::pgdate;"); resultSet.next(); ZonedDateTime expectedZdt = ZonedDateTime.of(2022, 5, 10, 0, 0, 0, 0, UTC_TZ.toZoneId()); @@ -253,7 +250,7 @@ void shouldReturnTimestampFromDate() throws SQLException { @Test void shouldCompareAllTimeStampsWithMultipleThreads() throws SQLException, InterruptedException, ExecutionException { - try (Connection connection = this.createConnection(); + try (Connection connection = createConnection(); Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery("SELECT CAST('1899-01-01 00:00:00' AS timestamptz);")) { resultSet.next(); diff --git a/src/integrationTest/java/integration/tests/client/RetryPolicyTest.java b/src/integrationTest/java/integration/tests/client/RetryPolicyTest.java index 44705dd6..5fbfb298 100644 --- a/src/integrationTest/java/integration/tests/client/RetryPolicyTest.java +++ b/src/integrationTest/java/integration/tests/client/RetryPolicyTest.java @@ -1,47 +1,29 @@ package integration.tests.client; -import static com.firebolt.jdbc.exception.ExceptionType.INVALID_REQUEST; -import static org.junit.jupiter.api.Assertions.*; - -import java.io.IOException; -import java.sql.SQLException; -import java.sql.Statement; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.FireboltException; - -import integration.IntegrationTest; +import integration.MockWebServerAwareIntegrationTest; import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; - -public class RetryPolicyTest extends IntegrationTest { - - private MockWebServer mockBackEnd; +import org.junit.jupiter.api.Test; - @BeforeEach - void setUp() throws IOException { - mockBackEnd = new MockWebServer(); - mockBackEnd.start(); - } +import java.sql.SQLException; +import java.sql.Statement; - @AfterEach - void tearDown() throws IOException { - mockBackEnd.close(); - } +import static com.firebolt.jdbc.exception.ExceptionType.INVALID_REQUEST; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +public class RetryPolicyTest extends MockWebServerAwareIntegrationTest { @Test public void shouldThrowExceptionOn400WithoutRetry() throws SQLException { mockBackEnd.enqueue(new MockResponse().setResponseCode(400)); try (FireboltConnection fireboltConnection = (FireboltConnection) createLocalConnection( - String.format("?ssl=0&port=%d&max_retries=%d", mockBackEnd.getPort(), 3)); + format("?ssl=0&port=%d&max_retries=%d", mockBackEnd.getPort(), 3)); Statement statement = fireboltConnection.createStatement()) { FireboltException ex = assertThrows(FireboltException.class, () -> statement.execute("SELECT 1;")); assertEquals(ex.getType(), INVALID_REQUEST); - assertEquals(1, mockBackEnd.getRequestCount()); + assertMockBackendRequestsCount(1); } } @@ -51,10 +33,10 @@ public void shouldRetryOn502() throws SQLException { mockBackEnd.enqueue(new MockResponse().setResponseCode(502)); mockBackEnd.enqueue(new MockResponse().setResponseCode(200)); try (FireboltConnection fireboltConnection = (FireboltConnection) createLocalConnection( - String.format("?ssl=0&port=%d&max_retries=%d", mockBackEnd.getPort(), 3)); + format("?ssl=0&port=%d&max_retries=%d", mockBackEnd.getPort(), 3)); Statement statement = fireboltConnection.createStatement()) { statement.execute("SELECT 1;"); - assertEquals(3, mockBackEnd.getRequestCount()); + assertMockBackendRequestsCount(3); } } diff --git a/src/integrationTest/java/integration/tests/client/TLSTest.java b/src/integrationTest/java/integration/tests/client/TLSTest.java index 58edfb26..c01db78f 100644 --- a/src/integrationTest/java/integration/tests/client/TLSTest.java +++ b/src/integrationTest/java/integration/tests/client/TLSTest.java @@ -1,6 +1,11 @@ package integration.tests.client; -import static org.junit.jupiter.api.Assertions.assertEquals; +import com.firebolt.jdbc.connection.FireboltConnection; +import integration.MockWebServerAwareIntegrationTest; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.tls.HandshakeCertificates; +import okhttp3.tls.HeldCertificate; +import org.junit.jupiter.api.Test; import java.io.File; import java.io.FileWriter; @@ -11,32 +16,7 @@ import java.sql.Statement; import java.util.UUID; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import com.firebolt.jdbc.connection.FireboltConnection; - -import integration.IntegrationTest; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.tls.HandshakeCertificates; -import okhttp3.tls.HeldCertificate; - -public class TLSTest extends IntegrationTest { - private MockWebServer mockBackEnd; - - @BeforeEach - void setUp() throws IOException { - mockBackEnd = new MockWebServer(); - mockBackEnd.start(); - } - - @AfterEach - void tearDown() throws IOException { - mockBackEnd.close(); - } - +public class TLSTest extends MockWebServerAwareIntegrationTest { @Test public void shouldUseTLS() throws SQLException, IOException, NoSuchFieldException, IllegalAccessException { mockBackEnd.enqueue(new MockResponse().setResponseCode(200)); @@ -61,7 +41,7 @@ public void shouldUseTLS() throws SQLException, IOException, NoSuchFieldExceptio // Write the public certificate to a file that will be used by the driver for // the TLS handshake - String path = this.getClass().getResource("/").getPath() + UUID.randomUUID() + ".pem"; + String path = getClass().getResource("/").getPath() + UUID.randomUUID() + ".pem"; try (Writer out = new FileWriter(new File(path).getAbsoluteFile())) { out.write(localhostCertificate.certificatePem()); } @@ -70,7 +50,7 @@ public void shouldUseTLS() throws SQLException, IOException, NoSuchFieldExceptio String.format("?ssl_certificate_path=%s&port=%s", path, mockBackEnd.getPort())); Statement statement = fireboltConnection.createStatement()) { statement.execute("SELECT 1;"); - assertEquals(1, mockBackEnd.getRequestCount()); + assertMockBackendRequestsCount(1); } finally { removeExistingClient(); } diff --git a/src/integrationTest/java/integration/tests/client/UsageTrackingTest.java b/src/integrationTest/java/integration/tests/client/UsageTrackingTest.java index a11e79ba..b1e54a2a 100644 --- a/src/integrationTest/java/integration/tests/client/UsageTrackingTest.java +++ b/src/integrationTest/java/integration/tests/client/UsageTrackingTest.java @@ -1,39 +1,19 @@ package integration.tests.client; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.io.IOException; -import java.sql.SQLException; -import java.sql.Statement; - -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.util.VersionUtil; - -import integration.IntegrationTest; +import integration.MockWebServerAwareIntegrationTest; import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.api.Test; -public class UsageTrackingTest extends IntegrationTest { - - private MockWebServer mockBackEnd; - - @BeforeEach - void setUp() throws IOException { - mockBackEnd = new MockWebServer(); - mockBackEnd.start(); - } +import java.sql.SQLException; +import java.sql.Statement; - @AfterEach - void tearDown() throws IOException { - mockBackEnd.close(); - } +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +public class UsageTrackingTest extends MockWebServerAwareIntegrationTest { @Test public void shouldSendRequestWithUserAgentHeaderContainingDriverAndClientInfo() throws SQLException, InterruptedException { @@ -44,9 +24,9 @@ public void shouldSendRequestWithUserAgentHeaderContainingDriverAndClientInfo() statement.execute("SELECT 1;"); RecordedRequest request = mockBackEnd.takeRequest(); String userAgentHeader = request.getHeaders().get("User-Agent"); - assertTrue(StringUtils.startsWith(userAgentHeader, - "GreatClient/0.1.4" + " JDBC/" + VersionUtil.getDriverVersion())); - assertTrue(StringUtils.endsWith(userAgentHeader, "AwesomeDriver/1.0.1")); + assertNotNull(userAgentHeader); + assertTrue(userAgentHeader.startsWith("GreatClient/0.1.4" + " JDBC/" + VersionUtil.getDriverVersion())); + assertTrue(userAgentHeader.endsWith("AwesomeDriver/1.0.1")); } } diff --git a/src/integrationTest/resources/statements/metadata/ddl.sql b/src/integrationTest/resources/statements/metadata/ddl.sql index a1c17b85..54fc457b 100644 --- a/src/integrationTest/resources/statements/metadata/ddl.sql +++ b/src/integrationTest/resources/statements/metadata/ddl.sql @@ -1,12 +1,12 @@ DROP TABLE IF EXISTS "integration_test" CASCADE; CREATE FACT TABLE IF NOT EXISTS "integration_test" ( - id BIGINT, + id BIGINT NOT NULL, ts timestamp NULL, tstz timestamptz NULL, tsntz timestampntz NULL, content text NULL, - success BOOLEAN NULL, - year int + success BOOLEAN NOT NULL, + year int NOT NULL ) primary index id \ No newline at end of file diff --git a/src/integrationTest/resources/statements/prepared-statement/ddl.sql b/src/integrationTest/resources/statements/prepared-statement/ddl.sql index ef9df1f1..37e159b5 100644 --- a/src/integrationTest/resources/statements/prepared-statement/ddl.sql +++ b/src/integrationTest/resources/statements/prepared-statement/ddl.sql @@ -1,7 +1,11 @@ DROP TABLE IF EXISTS prepared_statement_test; CREATE FACT TABLE IF NOT EXISTS prepared_statement_test ( -make STRING, -sales bigint + make STRING not null, + sales bigint not null, + ts timestamp NULL, + d date NULL, + signature bytea null, + url STRING null ) PRIMARY INDEX make; \ No newline at end of file diff --git a/src/integrationTest/resources/statements/statement-cancel/cleanup.sql b/src/integrationTest/resources/statements/statement-cancel/cleanup.sql index 2c68eab2..2792ee02 100644 --- a/src/integrationTest/resources/statements/statement-cancel/cleanup.sql +++ b/src/integrationTest/resources/statements/statement-cancel/cleanup.sql @@ -1,2 +1,3 @@ +DROP TABLE IF EXISTS "ex_lineitem" CASCADE; DROP TABLE IF EXISTS "first_statement_cancel_test" CASCADE; DROP TABLE IF EXISTS "second_statement_cancel_test" CASCADE; \ No newline at end of file diff --git a/src/integrationTest/resources/statements/statement-cancel/ddl.sql b/src/integrationTest/resources/statements/statement-cancel/ddl.sql index 97c2ccdd..c9b2d60d 100644 --- a/src/integrationTest/resources/statements/statement-cancel/ddl.sql +++ b/src/integrationTest/resources/statements/statement-cancel/ddl.sql @@ -1,8 +1,6 @@ +DROP TABLE IF EXISTS "ex_lineitem" CASCADE; DROP TABLE IF EXISTS "first_statement_cancel_test" CASCADE; DROP TABLE IF EXISTS "second_statement_cancel_test" CASCADE; -CREATE -EXTERNAL TABLE IF NOT EXISTS ex_lineitem ( l_orderkey LONG, l_partkey LONG, l_suppkey LONG, l_linenumber INT, l_quantity LONG, l_extendedprice LONG, l_discount LONG, l_tax LONG, l_returnflag TEXT, l_linestatus TEXT, l_shipdate TEXT, l_commitdate TEXT, l_receiptdate TEXT, l_shipinstruct TEXT, l_shipmode TEXT, l_comment TEXT)URL = 's3://firebolt-publishing-public/samples/tpc-h/parquet/lineitem/'OBJECT_PATTERN = '*.parquet'TYPE = (PARQUET); -CREATE -FACT TABLE IF NOT EXISTS first_statement_cancel_test ( l_orderkey LONG, l_partkey LONG, l_suppkey LONG, l_linenumber INT, l_quantity LONG, l_extendedprice LONG, l_discount LONG, l_tax LONG, l_returnflag TEXT, l_linestatus TEXT, l_shipdate TEXT, l_commitdate TEXT, l_receiptdate TEXT, l_shipinstruct TEXT, l_shipmode TEXT, l_comment TEXT ) PRIMARY INDEX l_orderkey, l_linenumber; -CREATE -FACT TABLE IF NOT EXISTS second_statement_cancel_test ( l_orderkey LONG, l_partkey LONG, l_suppkey LONG, l_linenumber INT, l_quantity LONG, l_extendedprice LONG, l_discount LONG, l_tax LONG, l_returnflag TEXT, l_linestatus TEXT, l_shipdate TEXT, l_commitdate TEXT, l_receiptdate TEXT, l_shipinstruct TEXT, l_shipmode TEXT, l_comment TEXT ) PRIMARY INDEX l_orderkey, l_linenumber; \ No newline at end of file +CREATE FACT TABLE IF NOT EXISTS ex_lineitem ( l_orderkey LONG ); +CREATE FACT TABLE IF NOT EXISTS first_statement_cancel_test ( l_orderkey LONG ) PRIMARY INDEX l_orderkey; +CREATE FACT TABLE IF NOT EXISTS second_statement_cancel_test ( l_orderkey LONG ) PRIMARY INDEX l_orderkey; \ No newline at end of file diff --git a/src/main/java/com/firebolt/FireboltDriver.java b/src/main/java/com/firebolt/FireboltDriver.java index 9800f5bd..d8537117 100644 --- a/src/main/java/com/firebolt/FireboltDriver.java +++ b/src/main/java/com/firebolt/FireboltDriver.java @@ -1,27 +1,28 @@ package com.firebolt; -import java.sql.*; -import java.util.Properties; -import java.util.logging.Logger; - -import org.apache.commons.lang3.StringUtils; - +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.util.LoggerUtil; import com.firebolt.jdbc.util.PropertyUtil; import com.firebolt.jdbc.util.VersionUtil; -import com.firebolt.jdbc.connection.FireboltConnection; -import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException; -import lombok.CustomLog; +import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverPropertyInfo; +import java.sql.SQLException; +import java.util.Properties; +import java.util.logging.Logger; -@CustomLog public class FireboltDriver implements Driver { public static final String JDBC_FIREBOLT = "jdbc:firebolt:"; - private static final String JDBC_FIREBOLT_PREFIX = JDBC_FIREBOLT + "//"; + private static final Logger rootLog; + private static final Logger log; static { try { java.sql.DriverManager.registerDriver(new FireboltDriver()); + rootLog = LoggerUtil.getRootLogger(); + log = Logger.getLogger(FireboltDriver.class.getName()); log.info("Firebolt Driver registered"); } catch (SQLException ex) { throw new RuntimeException("Cannot register the driver"); @@ -30,16 +31,16 @@ public class FireboltDriver implements Driver { @Override public Connection connect(String url, Properties connectionSettings) throws SQLException { - return acceptsURL(url) ? new FireboltConnection(url, connectionSettings) : null; + return acceptsURL(url) ? FireboltConnection.create(url, connectionSettings) : null; } @Override public boolean acceptsURL(String url) { - return StringUtils.isNotEmpty(url) && url.startsWith(JDBC_FIREBOLT_PREFIX); + return url != null && url.startsWith(JDBC_FIREBOLT); } @Override - public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) throws SQLException { + public DriverPropertyInfo[] getPropertyInfo(String url, Properties info) { return PropertyUtil.getPropertyInfo(url, info); } @@ -59,7 +60,7 @@ public boolean jdbcCompliant() { } @Override - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - throw new FireboltSQLFeatureNotSupportedException(); + public Logger getParentLogger() { + return rootLog; } } diff --git a/src/main/java/com/firebolt/jdbc/CheckedSupplier.java b/src/main/java/com/firebolt/jdbc/CheckedSupplier.java new file mode 100644 index 00000000..c08cfef4 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/CheckedSupplier.java @@ -0,0 +1,8 @@ +package com.firebolt.jdbc; + +import java.sql.SQLException; + +@FunctionalInterface +public interface CheckedSupplier { + R get() throws SQLException; +} \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/GenericWrapper.java b/src/main/java/com/firebolt/jdbc/GenericWrapper.java new file mode 100644 index 00000000..cb2f8059 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/GenericWrapper.java @@ -0,0 +1,19 @@ +package com.firebolt.jdbc; + +import java.sql.SQLException; +import java.sql.Wrapper; + +public interface GenericWrapper extends Wrapper { + @Override + default T unwrap(@SuppressWarnings("SpellCheckingInspection") Class iface) throws SQLException { + if (iface.isAssignableFrom(getClass())) { + return iface.cast(this); + } + throw new SQLException("Cannot unwrap to " + iface.getName()); + } + + @Override + default boolean isWrapperFor(@SuppressWarnings("SpellCheckingInspection") Class iface) { + return iface.isAssignableFrom(getClass()); + } +} diff --git a/src/main/java/com/firebolt/jdbc/JdbcBase.java b/src/main/java/com/firebolt/jdbc/JdbcBase.java new file mode 100644 index 00000000..450f8c45 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/JdbcBase.java @@ -0,0 +1,23 @@ +package com.firebolt.jdbc; + +import java.sql.SQLWarning; + +public abstract class JdbcBase implements GenericWrapper { + private SQLWarning firstWarning; + + public synchronized SQLWarning getWarnings() { + return firstWarning; + } + + public synchronized void clearWarnings() { + firstWarning = null; + } + + protected synchronized void addWarning(SQLWarning sqlWarning) { + if (firstWarning == null) { + firstWarning = sqlWarning; + } else { + firstWarning.setNextWarning(sqlWarning); + } + } +} diff --git a/src/main/java/com/firebolt/jdbc/Query.java b/src/main/java/com/firebolt/jdbc/Query.java deleted file mode 100644 index aa22b74b..00000000 --- a/src/main/java/com/firebolt/jdbc/Query.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.firebolt.jdbc; - -import java.util.Iterator; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; - -import lombok.Builder; -import lombok.Value; - -/** - * Represents a SQL query that can be sent to Firebolt - */ -@Builder -@Value -public class Query { - String select; - String from; - String innerJoin; - String orderBy; - List conditions; - - /** - * Parse the object to a SQL query that can be sent to Firebolt - * - * @return SQL query that can be sent to Firebolt - */ - public String toSql() { - StringBuilder query = new StringBuilder(); - if (StringUtils.isBlank(select)) { - throw new IllegalStateException("Cannot create query: SELECT cannot be blank"); - } - if (StringUtils.isBlank(from)) { - throw new IllegalStateException("Cannot create query: FROM cannot be blank"); - } - - query.append("SELECT ").append(select); - query.append(" FROM ").append(from); - if (StringUtils.isNotBlank(innerJoin)) { - query.append(" JOIN ").append(innerJoin); - } - query.append(getConditionsPart()); - if (StringUtils.isNotBlank(orderBy)) { - query.append(" order by ").append(orderBy); - } - return query.toString(); - } - - private String getConditionsPart() { - StringBuilder agg = new StringBuilder(); - Iterator iter = conditions.iterator(); - if (iter.hasNext()) { - agg.append(" WHERE "); - } - if (iter.hasNext()) { - String entry = iter.next(); - agg.append(entry); - } - while (iter.hasNext()) { - String entry = iter.next(); - agg.append(" AND ").append(entry); - } - return agg.toString(); - } -} diff --git a/src/main/java/com/firebolt/jdbc/QueryResult.java b/src/main/java/com/firebolt/jdbc/QueryResult.java index 0e2ccddc..fe3d7321 100644 --- a/src/main/java/com/firebolt/jdbc/QueryResult.java +++ b/src/main/java/com/firebolt/jdbc/QueryResult.java @@ -1,18 +1,20 @@ package com.firebolt.jdbc; +import com.firebolt.jdbc.type.FireboltDataType; +import lombok.Builder; +import lombok.Value; + import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Function; -import com.firebolt.jdbc.type.FireboltDataType; - -import lombok.Builder; -import lombok.Value; +import static java.util.stream.Collectors.toList; /** * Class containing a query result that can be used to create a * {@link com.firebolt.jdbc.resultset.FireboltResultSet} + * It is particularly useful for metadata methods as a ResulSet containing metadata info must be returned. */ @Builder @Value @@ -32,12 +34,13 @@ public class QueryResult { * TabSeparatedWithNamesAndTypes format */ @Override + @SuppressWarnings("java:S6204") // JDK 11 compatible public String toString() { StringBuilder stringBuilder = new StringBuilder(); - this.appendWithListValues(stringBuilder, columns.stream().map(Column::getName).collect(Collectors.toList())); + appendWithListValues(stringBuilder, columns.stream().map(Column::getName).collect(toList())); stringBuilder.append(NEXT_LINE); - this.appendWithListValues(stringBuilder, columns.stream().map(Column::getType) - .map(FireboltDataType::getAliases).map( aliases -> aliases[0]).collect(Collectors.toList())); + Function columnToString = c -> c.getType().getAliases()[0] + (c.isNullable() ? " null" : ""); + appendWithListValues(stringBuilder, columns.stream().map(columnToString).collect(toList())); stringBuilder.append(NEXT_LINE); for (int i = 0; i < rows.size(); i++) { @@ -76,5 +79,6 @@ private void appendWithListValues(StringBuilder destination, List values) { public static class Column { String name; FireboltDataType type; + boolean nullable; } } diff --git a/src/main/java/com/firebolt/jdbc/annotation/ExcludeFromJacocoGeneratedReport.java b/src/main/java/com/firebolt/jdbc/annotation/ExcludeFromJacocoGeneratedReport.java index 810d32b5..5c3d3422 100644 --- a/src/main/java/com/firebolt/jdbc/annotation/ExcludeFromJacocoGeneratedReport.java +++ b/src/main/java/com/firebolt/jdbc/annotation/ExcludeFromJacocoGeneratedReport.java @@ -5,7 +5,14 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Starting from JaCoCo 0.8.2, we can exclude classes and methods by annotating them with a custom annotation with the following properties: + *
    + *
  • The name of the annotation should include {@code Generated}.
  • + *
  • The retention policy of annotation should be runtime or class.
  • + *
+ */ @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD }) +@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD }) public @interface ExcludeFromJacocoGeneratedReport { } \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/client/FireboltClient.java b/src/main/java/com/firebolt/jdbc/client/FireboltClient.java index e5b6f1de..042d32af 100644 --- a/src/main/java/com/firebolt/jdbc/client/FireboltClient.java +++ b/src/main/java/com/firebolt/jdbc/client/FireboltClient.java @@ -1,11 +1,13 @@ package com.firebolt.jdbc.client; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.connection.CacheListener; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; +import com.firebolt.jdbc.exception.ServerError; +import com.firebolt.jdbc.exception.ServerError.Error.Location; import com.firebolt.jdbc.resultset.compress.LZ4InputStream; import com.firebolt.jdbc.util.CloseableUtil; -import lombok.CustomLog; import lombok.Getter; import lombok.NonNull; import okhttp3.Call; @@ -14,70 +16,97 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; +import org.json.JSONException; +import org.json.JSONObject; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.lang.reflect.Constructor; import java.nio.charset.StandardCharsets; +import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; +import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; +import static java.util.Optional.ofNullable; @Getter -@CustomLog -public abstract class FireboltClient { +public abstract class FireboltClient implements CacheListener { - public static final String HEADER_AUTHORIZATION = "Authorization"; - public static final String HEADER_AUTHORIZATION_BEARER_PREFIX_VALUE = "Bearer "; - public static final String HEADER_USER_AGENT = "User-Agent"; - protected final ObjectMapper objectMapper; - private final String headerUserAgentValue; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_AUTHORIZATION_BEARER_PREFIX_VALUE = "Bearer "; + private static final String HEADER_USER_AGENT = "User-Agent"; + private static final String HEADER_PROTOCOL_VERSION = "Firebolt-Protocol-Version"; + private static final Logger log = Logger.getLogger(FireboltClient.class.getName()); + private static final Pattern plainErrorPattern = Pattern.compile("Line (\\d+), Column (\\d+): (.*)$", Pattern.MULTILINE); private final OkHttpClient httpClient; - private final FireboltConnection connection; + private final String headerUserAgentValue; + protected final FireboltConnection connection; - protected FireboltClient(OkHttpClient httpClient, FireboltConnection connection, String customDrivers, - String customClients, ObjectMapper objectMapper) { + protected FireboltClient(OkHttpClient httpClient, FireboltConnection connection, String customDrivers, String customClients) { this.httpClient = httpClient; this.connection = connection; - this.objectMapper = objectMapper; this.headerUserAgentValue = UsageTrackerUtil.getUserAgentString(customDrivers != null ? customDrivers : "", customClients != null ? customClients : ""); + connection.register(this); + } + + protected T getResource(String uri, String accessToken, Class valueType) + throws IOException, SQLException { + return getResource(uri, uri, accessToken, valueType); } - protected T getResource(String uri, String host, String accessToken, Class valueType) - throws IOException, FireboltException { + protected T getResource(String uri, String host, String accessToken, Class valueType) throws SQLException, IOException { Request rq = createGetRequest(uri, accessToken); - try (Response response = this.execute(rq, host)) { - return objectMapper.readValue(getResponseAsString(response), valueType); + try (Response response = execute(rq, host)) { + return jsonToObject(getResponseAsString(response), valueType); } } + @SuppressWarnings("java:S3011") // setAccessible() is required here :( + protected T jsonToObject(String json, Class valueType) throws IOException { + try { + Constructor constructor = valueType.getDeclaredConstructor(JSONObject.class); + constructor.setAccessible(true); + return json == null ? null : constructor.newInstance(new JSONObject(json)); + } catch (ReflectiveOperationException | RuntimeException e) { + Throwable cause = Optional.ofNullable(e.getCause()).orElse(e); + throw new IOException(cause.getMessage(), cause); + } + } + private Request createGetRequest(String uri, String accessToken) { Request.Builder requestBuilder = new Request.Builder().url(uri); - this.createHeaders(accessToken) - .forEach(header -> requestBuilder.addHeader(header.getLeft(), header.getRight())); + createHeaders(accessToken).forEach(header -> requestBuilder.addHeader(header.getKey(), header.getValue())); return requestBuilder.build(); } - protected Response execute(@NonNull Request request, String host) throws IOException, FireboltException { + protected Response execute(@NonNull Request request, String host) throws IOException, SQLException { return execute(request, host, false); } protected Response execute(@NonNull Request request, String host, boolean isCompress) - throws IOException, FireboltException { + throws IOException, SQLException { Response response = null; try { - OkHttpClient client = getClientWithTimeouts(this.connection.getConnectionTimeout(), - this.connection.getNetworkTimeout()); + OkHttpClient client = getClientWithTimeouts(connection.getConnectionTimeout(), connection.getNetworkTimeout()); Call call = client.newCall(request); response = call.execute(); validateResponse(host, response, isCompress); @@ -89,83 +118,81 @@ protected Response execute(@NonNull Request request, String host, boolean isComp } private OkHttpClient getClientWithTimeouts(int connectionTimeout, int networkTimeout) { - if (connectionTimeout != this.httpClient.connectTimeoutMillis() - || networkTimeout != this.httpClient.readTimeoutMillis()) { + if (connectionTimeout != httpClient.connectTimeoutMillis() + || networkTimeout != httpClient.readTimeoutMillis()) { // This creates a shallow copy using the same connection pool - return this.httpClient.newBuilder().readTimeout(this.connection.getNetworkTimeout(), TimeUnit.MILLISECONDS) - .connectTimeout(this.connection.getConnectionTimeout(), TimeUnit.MILLISECONDS).build(); + return httpClient.newBuilder().readTimeout(connection.getNetworkTimeout(), TimeUnit.MILLISECONDS) + .connectTimeout(connection.getConnectionTimeout(), TimeUnit.MILLISECONDS).build(); } else { - return this.httpClient; + return httpClient; } } - protected Request createPostRequest(String uri, RequestBody requestBody) { - return createPostRequest(uri, requestBody, null, null); - } - - protected Request createPostRequest(String uri, RequestBody body, String accessToken, String id) { - Request.Builder requestBuilder = new Request.Builder().url(uri); - this.createHeaders(accessToken) - .forEach(header -> requestBuilder.addHeader(header.getLeft(), header.getRight())); + protected Request createPostRequest(String uri, String label, RequestBody body, String accessToken) { + Request.Builder requestBuilder = new Request.Builder().url(uri).tag(label); + createHeaders(accessToken).forEach(header -> requestBuilder.addHeader(header.getKey(), header.getValue())); if (body != null) { requestBuilder.post(body); } - if (id != null) { - requestBuilder.tag(id); - } return requestBuilder.build(); } - protected Request createPostRequest(String uri, String accessToken, String id) { - return createPostRequest(uri, (RequestBody) null, accessToken, id); - } - - protected Request createPostRequest(String uri, String json, String accessToken, String id) { + protected Request createPostRequest(String uri, String label, String json, String accessToken) { RequestBody requestBody = null; if (json != null) { requestBody = RequestBody.create(json, MediaType.parse("application/json")); } - return createPostRequest(uri, requestBody, accessToken, id); + return createPostRequest(uri, label, requestBody, accessToken); } - protected void validateResponse(String host, Response response, Boolean isCompress) throws FireboltException { + protected void validateResponse(String host, Response response, Boolean isCompress) throws SQLException { int statusCode = response.code(); if (!isCallSuccessful(statusCode)) { if (statusCode == HTTP_UNAVAILABLE) { - throw new FireboltException( - String.format("Could not query Firebolt at %s. The engine is not running.", host), statusCode); + throw new FireboltException(format("Could not query Firebolt at %s. The engine is not running.", host), + statusCode, SQLState.CONNECTION_FAILURE); } - String errorResponseMessage; - try { - String errorMessageFromServer = extractErrorMessage(response, isCompress); - errorResponseMessage = String.format( - "Server failed to execute query with the following error:%n%s%ninternal error:%n%s", - errorMessageFromServer, this.getInternalErrorWithHeadersText(response)); - if (statusCode == HTTP_UNAUTHORIZED) { - this.getConnection().removeExpiredTokens(); - throw new FireboltException(String.format( - "Could not query Firebolt at %s. The operation is not authorized or the token is expired and has been cleared from the cache.%n%s", - host, errorResponseMessage), statusCode, errorMessageFromServer); - } - throw new FireboltException(errorResponseMessage, statusCode, errorMessageFromServer); - } catch (IOException e) { - log.warn("Could not parse response containing the error message from Firebolt", e); - errorResponseMessage = String.format("Server failed to execute query%ninternal error:%n%s", - this.getInternalErrorWithHeadersText(response)); - throw new FireboltException(errorResponseMessage, statusCode, e); + String errorMessageFromServer = extractErrorMessage(response, isCompress); + ServerError serverError = parseServerError(errorMessageFromServer); + String processedErrorMessage = serverError.getErrorMessage(); + validateResponse(host, statusCode, processedErrorMessage); + String errorResponseMessage = format( + "Server failed to execute query with the following error:%n%s%ninternal error:%n%s", + processedErrorMessage, getInternalErrorWithHeadersText(response)); + if (statusCode == HTTP_UNAUTHORIZED || statusCode == HTTP_FORBIDDEN) { + getConnection().removeExpiredTokens(); + throw new FireboltException(format( + "Could not query Firebolt at %s. The operation is not authorized or the token is expired and has been cleared from the cache.%n%s", + host, errorResponseMessage), statusCode, processedErrorMessage, + SQLState.INVALID_AUTHORIZATION_SPECIFICATION); } + throw new FireboltException(errorResponseMessage, statusCode, processedErrorMessage); } } - protected String getResponseAsString(Response response) throws FireboltException, IOException { + protected void validateResponse(String host, int statusCode, String errorMessageFromServer) throws SQLException { + // empty implementation + } + + protected String getResponseAsString(Response response) throws SQLException, IOException { if (response.body() == null) { throw new FireboltException("Cannot get resource: the response from the server is empty"); } return response.body().string(); } - private String extractErrorMessage(Response response, boolean isCompress) throws IOException { - byte[] entityBytes = response.body() != null ? response.body().bytes() : null; + @SuppressWarnings("java:S2139") // TODO: Exceptions should be either logged or rethrown but not both + private String extractErrorMessage(Response response, boolean isCompress) throws SQLException { + byte[] entityBytes; + try { + entityBytes = response.body() != null ? response.body().bytes() : null; + } catch (IOException e) { + log.log(Level.WARNING, "Could not parse response containing the error message from Firebolt", e); + String errorResponseMessage = format("Server failed to execute query%ninternal error:%n%s", + getInternalErrorWithHeadersText(response)); + throw new FireboltException(errorResponseMessage, response.code(), e); + } + if (entityBytes == null) { return null; } @@ -175,21 +202,65 @@ private String extractErrorMessage(Response response, boolean isCompress) throws return new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)).lines() .collect(Collectors.joining("\n")) + "\n"; } catch (Exception e) { - log.warn("Could not decompress error from server"); + log.log(Level.WARNING, "Could not decompress error from server"); } } return new String(entityBytes, StandardCharsets.UTF_8); } - private boolean isCallSuccessful(int statusCode) { + private ServerError parseServerError(String responseText) { + try { + if (responseText == null) { + return new ServerError(null, null); + } + ServerError serverError = new ServerError(new JSONObject(responseText)); + ServerError.Error[] errors = serverError.getErrors() == null ? null : Arrays.stream(serverError.getErrors()).map(this::updateError).toArray(ServerError.Error[]::new); + return new ServerError(serverError.getQuery(), errors); + } catch (JSONException e) { + String message = responseText; + Location location = null; + Entry locationAndText = getLocationFromMessage(responseText); + if (locationAndText != null) { + location = locationAndText.getKey(); + message = locationAndText.getValue(); + } + return new ServerError(null, new ServerError.Error[] {new ServerError.Error(null, message, null, null, null, null, null, location)}); + } + } + + private ServerError.Error updateError(ServerError.Error error) { + if (error == null || error.getDescription() == null) { + return error; + } + Entry locationAndText = getLocationFromMessage(error.getDescription()); + if (locationAndText == null) { + return error; + } + Location location = Objects.requireNonNullElse(error.getLocation(), locationAndText.getKey()); + return new ServerError.Error(error.getCode(), error.getName(), error.getSeverity(), error.getSource(), + locationAndText.getValue(), error.getResolution(), error.getHelpLink(), location); + } + + private Entry getLocationFromMessage(String responseText) { + Matcher m = plainErrorPattern.matcher(responseText); + if (m.find()) { + int line = Integer.parseInt(m.group(1)); + int column = Integer.parseInt(m.group(2)); + String message = m.group(3); + return Map.entry(new Location(line, column, column), message); + } + return null; + } + + protected boolean isCallSuccessful(int statusCode) { return statusCode >= 200 && statusCode <= 299; // Call is considered successful when the status code is 2XX } - private List> createHeaders(String accessToken) { - List> headers = new ArrayList<>(); - headers.add(new ImmutablePair<>(HEADER_USER_AGENT, this.getHeaderUserAgentValue())); - Optional.ofNullable(accessToken).ifPresent(token -> headers.add( - new ImmutablePair<>(HEADER_AUTHORIZATION, HEADER_AUTHORIZATION_BEARER_PREFIX_VALUE + accessToken))); + private List> createHeaders(String accessToken) { + List> headers = new ArrayList<>(); + headers.add(Map.entry(HEADER_USER_AGENT, getHeaderUserAgentValue())); + ofNullable(connection.getProtocolVersion()).ifPresent(version -> headers.add(Map.entry(HEADER_PROTOCOL_VERSION, version))); + ofNullable(accessToken).ifPresent(token -> headers.add(Map.entry(HEADER_AUTHORIZATION, HEADER_AUTHORIZATION_BEARER_PREFIX_VALUE + accessToken))); return headers; } @@ -197,4 +268,8 @@ private String getInternalErrorWithHeadersText(Response response) { return response.toString() + "\n" + response.headers(); } + @Override + public void cleanup() { + // empty default implementation + } } diff --git a/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java b/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java deleted file mode 100644 index dbd2c843..00000000 --- a/src/main/java/com/firebolt/jdbc/client/FireboltObjectMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.firebolt.jdbc.client; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -public class FireboltObjectMapper { - private static final ObjectMapper MAPPER = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - private FireboltObjectMapper() { - } - - public static com.fasterxml.jackson.databind.ObjectMapper getInstance() { - return MAPPER; - } -} diff --git a/src/main/java/com/firebolt/jdbc/client/HttpClientConfig.java b/src/main/java/com/firebolt/jdbc/client/HttpClientConfig.java index 7849c65a..557b0746 100644 --- a/src/main/java/com/firebolt/jdbc/client/HttpClientConfig.java +++ b/src/main/java/com/firebolt/jdbc/client/HttpClientConfig.java @@ -1,20 +1,18 @@ package com.firebolt.jdbc.client; +import com.firebolt.jdbc.client.config.OkHttpClientCreator; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import okhttp3.OkHttpClient; + import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; +import java.util.logging.Logger; -import com.firebolt.jdbc.client.config.OkHttpClientCreator; -import com.firebolt.jdbc.connection.settings.FireboltProperties; - -import lombok.CustomLog; -import okhttp3.OkHttpClient; - -@CustomLog public class HttpClientConfig { - + private static final Logger log = Logger.getLogger(HttpClientConfig.class.getName()); private static OkHttpClient instance; private HttpClientConfig() { diff --git a/src/main/java/com/firebolt/jdbc/client/UsageTrackerUtil.java b/src/main/java/com/firebolt/jdbc/client/UsageTrackerUtil.java index 0d6312fb..409c07dd 100644 --- a/src/main/java/com/firebolt/jdbc/client/UsageTrackerUtil.java +++ b/src/main/java/com/firebolt/jdbc/client/UsageTrackerUtil.java @@ -1,17 +1,17 @@ package com.firebolt.jdbc.client; import com.firebolt.jdbc.util.VersionUtil; -import lombok.CustomLog; import lombok.experimental.UtilityClass; -import org.apache.commons.lang3.StringUtils; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; -@CustomLog @UtilityClass public class UsageTrackerUtil { + private static final Logger log = Logger.getLogger(UsageTrackerUtil.class.getName()); public static final Map CLIENT_MAP = Map.of( "Tableau", "com.tableau", "Looker", "com.looker", @@ -25,7 +25,7 @@ private static String getVersionForClass(String name) { Class c = Class.forName(name); return c.getPackage().getImplementationVersion(); } catch (ClassNotFoundException e) { - log.debug("Unable to get version for class " + name); + log.log(Level.FINE, "Unable to get version for class {0}", name); return ""; } } @@ -37,15 +37,15 @@ public Map getClients(StackTraceElement[] stack, Map connectorEntry : clientMap.entrySet()) { - if (StringUtils.contains(s.getClassName(), connectorEntry.getValue())) { + if (s.getClassName().contains(connectorEntry.getValue())) { String version = getVersionForClass(s.getClassName()); - log.debug("Detected running from " + connectorEntry.getKey() + " Version " + version); + log.log(Level.FINE, "Detected running from {0} Version {1}", new Object[] {connectorEntry.getKey(), version}); clients.put(connectorEntry.getKey(), version); } } } if (clients.isEmpty()) { - log.debug("No clients detected for tracking"); + log.log(Level.FINE, "No clients detected for tracking"); } return clients; } @@ -60,7 +60,7 @@ private static Map extractNameToVersion(String namesAndVersions) nameToVersion.put(connectorInfo[0], connectorInfo[1]); } } else { - log.debug(String.format("Incorrect connector format is provided: %s, Expected: ConnA:1.0.2,ConnB:2.9.3", namesAndVersions)); + log.log(Level.FINE, "Incorrect connector format is provided: {0}, Expected: ConnA:1.0.2,ConnB:2.9.3", namesAndVersions); } return nameToVersion; } diff --git a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccount.java b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccount.java new file mode 100644 index 00000000..3a1750df --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccount.java @@ -0,0 +1,47 @@ +package com.firebolt.jdbc.client.account; + +import org.json.JSONObject; + +import java.util.Objects; + +public class FireboltAccount { + private final String id; + private final String region; + private final int infraVersion; + + public FireboltAccount(String id, String region, int infraVersion) { + this.id = id; + this.region = region; + this.infraVersion = infraVersion; + } + + @SuppressWarnings("unused") // used by FireboltAccountRetriever that in turn calls its base class` method FireboltClient.jsonToObject() that calls this constructor by reflection + FireboltAccount(JSONObject json) { + this(json.getString("id"), json.getString("region"), json.optInt("infraVersion", 1)); + } + + public String getId() { + return id; + } + + public String getRegion() { + return region; + } + + public int getInfraVersion() { + return infraVersion; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FireboltAccount account = (FireboltAccount) o; + return Objects.equals(id, account.id) && Objects.equals(region, account.region) && Objects.equals(infraVersion, account.infraVersion); + } + + @Override + public int hashCode() { + return Objects.hash(id, region, infraVersion); + } +} diff --git a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java index 0b020a89..eb0ff73d 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java +++ b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountClient.java @@ -1,10 +1,5 @@ package com.firebolt.jdbc.client.account; -import java.io.IOException; - -import org.apache.commons.lang3.StringUtils; - -import com.fasterxml.jackson.databind.ObjectMapper; import com.firebolt.jdbc.client.FireboltClient; import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; @@ -13,121 +8,110 @@ import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; - -import lombok.CustomLog; import okhttp3.OkHttpClient; -@CustomLog +import java.io.IOException; +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.lang.String.format; + public class FireboltAccountClient extends FireboltClient { - private static final String GET_ACCOUNT_ID_URI = "%s/iam/v2/accounts:getIdByName?accountName=%s"; - private static final String URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME = "engines:getIdByName?engine_name="; - private static final String URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID = "engines/"; - private static final String URI_SUFFIX_DATABASE_INFO_URL = "engines:getURLByDatabaseName?databaseName="; - private static final String URI_PREFIX_WITH_ACCOUNT_RESOURCE = "%s/core/v1/accounts/%s/%s"; - private static final String URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE = "%s/core/v1/account/%s"; + private static final String GET_ACCOUNT_ID_URI = "%s/iam/v2/accounts:getIdByName?accountName=%s"; + private static final String URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME = "engines:getIdByName?engine_name="; + private static final String URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID = "engines/"; + private static final String URI_SUFFIX_DATABASE_INFO_URL = "engines:getURLByDatabaseName?databaseName="; + private static final String URI_PREFIX_WITH_ACCOUNT_RESOURCE = "%s/core/v1/accounts/%s/%s"; + private static final String URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE = "%s/core/v1/account/%s"; + private static final Map resourceCache = new ConcurrentHashMap<>(); + + public FireboltAccountClient(OkHttpClient httpClient, FireboltConnection fireboltConnection, String customDrivers, String customClients) { + super(httpClient, fireboltConnection, customDrivers, customClients); + } - public FireboltAccountClient(OkHttpClient httpClient, ObjectMapper objectMapper, - FireboltConnection fireboltConnection, String customDrivers, String customClients) { - super(httpClient, fireboltConnection, customDrivers, customClients, objectMapper); - } + /** + * Returns the account + * + * @param host the host + * @param account the name of the account + * @param accessToken the access token + * @return the account + */ + @SuppressWarnings("java:S3824") // cannot use computeIfAbsent() because getResource() throws checked exceptions + public FireboltAccountResponse getAccount(String host, String account, String accessToken) throws SQLException, IOException { + String uri = format(GET_ACCOUNT_ID_URI, host, account); + FireboltAccountResponse accountResponse = (FireboltAccountResponse)resourceCache.get(uri); + if (accountResponse == null) { + accountResponse = getResource(uri, host, accessToken, FireboltAccountResponse.class); + resourceCache.put(uri, accountResponse); + } + return accountResponse; + } - /** - * Returns the account - * - * @param host the host - * @param account the name of the account - * @param accessToken the access token - * @return the account - */ - public FireboltAccountResponse getAccount(String host, String account, String accessToken) - throws FireboltException, IOException { - String uri = String.format(GET_ACCOUNT_ID_URI, host, account); - return getResource(uri, host, accessToken, FireboltAccountResponse.class); - } + /** + * Returns an engine + * + * @param host the host + * @param accountId the id of the account + * @param engineName the engine name + * @param engineId the engine id + * @param accessToken the access token + * @return the engine + */ + public FireboltEngineResponse getEngine(String host, String accountId, String engineName, String engineId, String accessToken) throws SQLException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID + engineId); + return getResource(uri, host, accessToken, FireboltEngineResponse.class, format("The address of the engine with name %s and id %s could not be found", engineName, engineId)); + } - /** - * Returns an engine - * - * @param host the host - * @param accountId the id of the account - * @param engineName the engine name - * @param engineId the engine id - * @param accessToken the access token - * @return the engine - */ - public FireboltEngineResponse getEngine(String host, String accountId, String engineName, String engineId, - String accessToken) throws FireboltException, IOException { - try { - String uri = createAccountUri(accountId, host, URI_SUFFIX_ACCOUNT_ENGINE_INFO_BY_ENGINE_ID + engineId); - return getResource(uri, host, accessToken, FireboltEngineResponse.class); - } catch (FireboltException exception) { - if (exception.getType() == ExceptionType.RESOURCE_NOT_FOUND) { - throw new FireboltException( - String.format("The address of the engine with name %s and id %s could not be found", engineName, - engineId), - exception, ExceptionType.RESOURCE_NOT_FOUND); - } else { - throw exception; - } - } - } + /** + * Returns the default engine of the database + * + * @param host the host + * @param accountId the account id + * @param dbName the name of the database + * @param accessToken the access token + * @return the default engine for the database + */ + public FireboltDefaultDatabaseEngineResponse getDefaultEngineByDatabaseName(String host, String accountId, String dbName, String accessToken) throws SQLException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_DATABASE_INFO_URL + dbName); + return getResource(uri, host, accessToken, FireboltDefaultDatabaseEngineResponse.class, format("The database with the name %s could not be found", dbName)); + } - /** - * Returns the default engine of the database - * - * @param host the host - * @param accountId the account id - * @param dbName the name of the database - * @param accessToken the access token - * @return the default engine for the database - */ - public FireboltDefaultDatabaseEngineResponse getDefaultEngineByDatabaseName(String host, String accountId, String dbName, - String accessToken) throws FireboltException, IOException { - String uri = createAccountUri(accountId, host, URI_SUFFIX_DATABASE_INFO_URL + dbName); - try { - return getResource(uri, host, accessToken, FireboltDefaultDatabaseEngineResponse.class); + /** + * Returns the engine id + * + * @param host the host + * @param accountId the account id + * @param engineName the name of the engine + * @param accessToken the access token + * @return the engine id + */ + public FireboltEngineIdResponse getEngineId(String host, String accountId, String engineName, String accessToken) + throws SQLException, IOException { + String uri = createAccountUri(accountId, host, URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME + engineName); + return getResource(uri, host, accessToken, FireboltEngineIdResponse.class, format("The engine %s could not be found", engineName)); + } - } catch (FireboltException exception) { - if (exception.getType() == ExceptionType.RESOURCE_NOT_FOUND) { - throw new FireboltException(String.format("The database with the name %s could not be found", dbName), - exception, ExceptionType.RESOURCE_NOT_FOUND); - } else { - throw exception; - } - } - } - /** - * Returns the engine id - * - * @param host the host - * @param accountId the account id - * @param engineName the name of the engine - * @param accessToken the access token - * @return the engine id - */ - public FireboltEngineIdResponse getEngineId(String host, String accountId, String engineName, String accessToken) - throws FireboltException, IOException { - try { - String uri = createAccountUri(accountId, host, - URI_SUFFIX_ENGINE_AND_ACCOUNT_ID_BY_ENGINE_NAME + engineName); - return getResource(uri, host, accessToken, FireboltEngineIdResponse.class); - } catch (FireboltException exception) { - if (exception.getType() == ExceptionType.RESOURCE_NOT_FOUND) { - throw new FireboltException(String.format("The engine %s could not be found", engineName), exception, - ExceptionType.RESOURCE_NOT_FOUND); - } else { - throw exception; - } - } - } + private R getResource(String uri, String host, String accessToken, Class responseType, String notFoundErrorMessage) throws SQLException, IOException { + try { + return getResource(uri, host, accessToken, responseType); + } catch (FireboltException exception) { + if (exception.getType() == ExceptionType.RESOURCE_NOT_FOUND) { + throw new FireboltException(notFoundErrorMessage, exception, ExceptionType.RESOURCE_NOT_FOUND); + } + throw exception; + } + } - private String createAccountUri(String account, String host, String suffix) { - if (StringUtils.isNotEmpty(account)) - return String.format(URI_PREFIX_WITH_ACCOUNT_RESOURCE, host, account, suffix); - else - return String.format(URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE, host, suffix); - } + private String createAccountUri(String account, String host, String suffix) { + return account == null || account.isEmpty() ? format(URI_PREFIX_WITHOUT_ACCOUNT_RESOURCE, host, suffix) : format(URI_PREFIX_WITH_ACCOUNT_RESOURCE, host, account, suffix); + } -} + @Override + public void cleanup() { + resourceCache.clear(); + } +} \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java new file mode 100644 index 00000000..8b4876af --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/account/FireboltAccountRetriever.java @@ -0,0 +1,65 @@ +package com.firebolt.jdbc.client.account; + +import com.firebolt.jdbc.client.FireboltClient; +import com.firebolt.jdbc.connection.CacheListener; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.FireboltException; +import okhttp3.OkHttpClient; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; + +public class FireboltAccountRetriever extends FireboltClient implements CacheListener { + private static final String URL = "https://%s/web/v3/account/%s/%s"; + private final String host; + private final String path; + private final Class type; + private static final Map valueCache = new ConcurrentHashMap<>(); + + public FireboltAccountRetriever(OkHttpClient httpClient, FireboltConnection connection, String customDrivers, String customClients, String host, String path, Class type) { + super(httpClient, connection, customDrivers, customClients); + this.host = host; + this.path = path; + this.type = type; + } + + public T retrieve(String accessToken, String accountName) throws SQLException { + try { + String url = format(URL, host, accountName, path); + @SuppressWarnings("unchecked") + T value = (T)valueCache.get(url); + if (value == null) { + value = getResource(url, accessToken, type); + valueCache.put(url, value); + } + return value; + } catch (IOException e) { + throw new FireboltException(String.format("Failed to get %s url for account %s: %s", path, accountName, e.getMessage()), e); + } + } + + @Override + protected void validateResponse(String host, int statusCode, String errorMessageFromServer) throws SQLException { + if (statusCode == HTTP_NOT_FOUND) { + String[] fragments = host.split("/"); + // Second to last because th last element presents action and the second to last is the account name + // The validation of the array length is done "just in case" to be safe for future, probably wrong, modifications + // because the last thing we want is to fail on ArrayIndexOutOfBounds when creating error message. + String account = fragments.length > 1 ? fragments[fragments.length - 2] : "N/A"; + throw new FireboltException( + format("Account '%s' does not exist in this organization or is not authorized. " + + "Please verify the account name and make sure your service account has the correct RBAC permissions and is linked to a user.", account), + statusCode, errorMessageFromServer); + } + } + + @Override + public void cleanup() { + valueCache.clear(); + } +} diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java index effb2e4c..0fd3f2e0 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltAccountResponse.java @@ -1,13 +1,19 @@ package com.firebolt.jdbc.client.account.response; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.json.JSONObject; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder public class FireboltAccountResponse { - @JsonProperty("account_id") - String accountId; + private final String accountId; + + public FireboltAccountResponse(String accountId) { + this.accountId = accountId; + } + + FireboltAccountResponse(JSONObject json) { + this(json.getString("account_id")); + } + + public String getAccountId() { + return accountId; + } } diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java index e4fa1a88..be92f083 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltDefaultDatabaseEngineResponse.java @@ -1,13 +1,19 @@ package com.firebolt.jdbc.client.account.response; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.json.JSONObject; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder public class FireboltDefaultDatabaseEngineResponse { - @JsonProperty("engine_url") - String engineUrl; + private final String engineUrl; + + public FireboltDefaultDatabaseEngineResponse(String engineUrl) { + this.engineUrl = engineUrl; + } + + FireboltDefaultDatabaseEngineResponse(JSONObject json) { + this(json.getString("engine_url")); + } + + public String getEngineUrl() { + return engineUrl; + } } diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineIdResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineIdResponse.java index c02f3fb8..69fa3cbb 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineIdResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineIdResponse.java @@ -1,20 +1,35 @@ package com.firebolt.jdbc.client.account.response; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.json.JSONObject; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder public class FireboltEngineIdResponse { - @JsonProperty("engine_id") - Engine engine; + private final Engine engine; + + public FireboltEngineIdResponse(Engine engine) { + this.engine = engine; + } + + FireboltEngineIdResponse(JSONObject json) { + this(new Engine(json.getJSONObject("engine_id"))); + } + + public Engine getEngine() { + return engine; + } - @Value - @Builder public static class Engine { - @JsonProperty("engine_id") - String engineId; + private final String engineId; + + public Engine(String engineId) { + this.engineId = engineId; + } + + private Engine(JSONObject json) { + this(json.getString("engine_id")); + } + + public String getEngineId() { + return engineId; + } } } diff --git a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineResponse.java b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineResponse.java index 1aab8be9..fa553512 100644 --- a/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineResponse.java +++ b/src/main/java/com/firebolt/jdbc/client/account/response/FireboltEngineResponse.java @@ -1,20 +1,41 @@ package com.firebolt.jdbc.client.account.response; -import com.fasterxml.jackson.annotation.JsonProperty; +import org.json.JSONObject; -import lombok.Builder; -import lombok.Value; - -@Value -@Builder public class FireboltEngineResponse { - Engine engine; + private final Engine engine; + + public FireboltEngineResponse(Engine engine) { + this.engine = engine; + } + + FireboltEngineResponse(JSONObject json) { + this(new Engine(json.getJSONObject("engine"))); + } + + public Engine getEngine() { + return engine; + } - @Value - @Builder public static class Engine { - String endpoint; - @JsonProperty("current_status") - String currentStatus; + private final String endpoint; + private final String currentStatus; + + public Engine(String endpoint, String currentStatus) { + this.endpoint = endpoint; + this.currentStatus = currentStatus; + } + + private Engine(JSONObject json) { + this(json.getString("endpoint"), json.getString("current_status")); + } + + public String getEndpoint() { + return endpoint; + } + + public String getCurrentStatus() { + return currentStatus; + } } } diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequest.java index 4989e40b..ad970748 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequest.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequest.java @@ -1,11 +1,9 @@ package com.firebolt.jdbc.client.authentication; -import com.fasterxml.jackson.core.JsonProcessingException; - import okhttp3.RequestBody; public interface AuthenticationRequest { - RequestBody getRequestBody() throws JsonProcessingException; + RequestBody getRequestBody(); String getUri(); } \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactory.java b/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactory.java deleted file mode 100644 index 0f432c70..00000000 --- a/src/main/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.firebolt.jdbc.client.authentication; - -import org.apache.commons.lang3.StringUtils; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class AuthenticationRequestFactory { - - public static AuthenticationRequest getAuthenticationRequest(String username, String password, String host) { - if (StringUtils.isEmpty(username) || StringUtils.contains(username, "@")) { - return new UsernamePasswordAuthenticationRequest(username, password, host); - } else { - return new ServiceAccountAuthenticationRequest(username, password, host); - } - } - -} diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java b/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java index 3cf6d614..3c28151b 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClient.java @@ -1,24 +1,23 @@ package com.firebolt.jdbc.client.authentication; -import com.fasterxml.jackson.databind.ObjectMapper; import com.firebolt.jdbc.client.FireboltClient; -import com.firebolt.jdbc.client.authentication.response.FireboltAuthenticationResponse; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.FireboltConnectionTokens; -import com.firebolt.jdbc.exception.FireboltException; -import lombok.CustomLog; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import java.io.IOException; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; -@CustomLog -public class FireboltAuthenticationClient extends FireboltClient { +public abstract class FireboltAuthenticationClient extends FireboltClient { + private static final Logger log = Logger.getLogger(FireboltAuthenticationClient.class.getName()); - public FireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper objectMapper, - FireboltConnection connection, String customDrivers, String customClients) { - super(httpClient, connection, customDrivers, customClients, objectMapper); + protected FireboltAuthenticationClient(OkHttpClient httpClient, + FireboltConnection connection, String customDrivers, String customClients) { + super(httpClient, connection, customDrivers, customClients); } /** @@ -27,40 +26,36 @@ public FireboltAuthenticationClient(OkHttpClient httpClient, ObjectMapper object * @param host the host * @param user the username * @param password the password + * @param environment the environment * @return the connection tokens */ - public FireboltConnectionTokens postConnectionTokens(String host, String user, String password) - throws IOException, FireboltException { - AuthenticationRequest authenticationRequest = AuthenticationRequestFactory.getAuthenticationRequest(user, - password, host); + public FireboltConnectionTokens postConnectionTokens(String host, String user, String password, String environment) + throws SQLException, IOException { + AuthenticationRequest authenticationRequest = getAuthenticationRequest(user, password, host, environment); String uri = authenticationRequest.getUri(); - log.debug("Creating connection with url {}", uri); - Request request = this.createPostRequest(uri, authenticationRequest.getRequestBody()); - try (Response response = this.execute(request, host)) { + log.log(Level.FINE, "Creating connection with url {0}", uri); + Request request = createPostRequest(uri, null, authenticationRequest.getRequestBody(), null); + try (Response response = execute(request, host)) { String responseString = getResponseAsString(response); - FireboltAuthenticationResponse authenticationResponse = objectMapper.readValue(responseString, - FireboltAuthenticationResponse.class); - FireboltConnectionTokens authenticationTokens = FireboltConnectionTokens.builder() - .accessToken(authenticationResponse.getAccessToken()) - .refreshToken(authenticationResponse.getRefreshToken()) - .expiresInSeconds(authenticationResponse.getExpiresIn()).build(); + FireboltConnectionTokens authenticationTokens = jsonToObject(responseString, FireboltConnectionTokens.class); log.info("Successfully fetched connection token"); - logToken(authenticationResponse); + logToken(authenticationTokens); return authenticationTokens; } } - private void logToken(FireboltAuthenticationResponse connectionTokens) { + private void logToken(FireboltConnectionTokens connectionTokens) { logIfPresent(connectionTokens.getAccessToken(), "Retrieved access_token"); - logIfPresent(connectionTokens.getRefreshToken(), "Retrieved refresh_token"); - if (connectionTokens.getExpiresIn() >=- 0) { - log.debug("Retrieved expires_in"); + if (connectionTokens.getExpiresInSeconds() >=- 0) { + log.log(Level.FINE, "Retrieved expires_in"); } } private void logIfPresent(String token, String message) { if (token != null && !token.isEmpty()) { - log.debug(message); + log.log(Level.FINE, message); } } + + protected abstract AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment); } diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java index a6bf9918..89f70aea 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequest.java @@ -1,27 +1,42 @@ package com.firebolt.jdbc.client.authentication; -import lombok.AllArgsConstructor; import okhttp3.FormBody; import okhttp3.RequestBody; -@AllArgsConstructor +import static java.lang.String.format; + public class ServiceAccountAuthenticationRequest implements AuthenticationRequest { - private static final String CLIENT_CREDENTIALS = "client_credentials"; - private static final String GRAND_TYPE_FIELD_NAME = "grant_type"; - private static final String CLIENT_ID_FIELD_NAME = "client_id"; - private static final String CLIENT_SECRET_FIELD_NAME = "client_secret"; - private static final String AUTH_URL = "%s/auth/v1/token"; - private String id; - private String secret; - private String host; - - public RequestBody getRequestBody() { - return new FormBody.Builder().add(CLIENT_ID_FIELD_NAME, id).add(CLIENT_SECRET_FIELD_NAME, secret) - .add(GRAND_TYPE_FIELD_NAME, CLIENT_CREDENTIALS).build(); - } - - @Override - public String getUri() { - return String.format(AUTH_URL, host); - } + + private static final String AUDIENCE_FIELD_NAME = "audience"; + private static final String AUDIENCE_FIELD_VALUE = "https://api.firebolt.io"; + private static final String GRAND_TYPE_FIELD_NAME = "grant_type"; + private static final String GRAND_TYPE_FIELD_VALUE = "client_credentials"; + private static final String CLIENT_ID_FIELD_NAME = "client_id"; + private static final String CLIENT_SECRET_FIELD_NAME = "client_secret"; + private static final String AUTH_URL = "https://id.%s.firebolt.io/oauth/token"; + + private final String clientId; + private final String clientSecret; + private final String environment; + + public ServiceAccountAuthenticationRequest(String clientId, String clientSecret, String environment) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.environment = environment; + } + + @Override + public RequestBody getRequestBody() { + return new FormBody.Builder() + .add(AUDIENCE_FIELD_NAME, AUDIENCE_FIELD_VALUE) + .add(GRAND_TYPE_FIELD_NAME, GRAND_TYPE_FIELD_VALUE) + .addEncoded(CLIENT_ID_FIELD_NAME, clientId) + .addEncoded(CLIENT_SECRET_FIELD_NAME, clientSecret) + .build(); + } + + @Override + public String getUri() { + return format(AUTH_URL, environment); + } } diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java b/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java index 1ef2b31e..2ae774a3 100644 --- a/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java +++ b/src/main/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequest.java @@ -1,35 +1,31 @@ package com.firebolt.jdbc.client.authentication; -import java.util.HashMap; -import java.util.Map; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.firebolt.jdbc.client.FireboltObjectMapper; - import lombok.AllArgsConstructor; import okhttp3.MediaType; import okhttp3.RequestBody; +import org.json.JSONObject; + +import static java.lang.String.format; @AllArgsConstructor public class UsernamePasswordAuthenticationRequest implements AuthenticationRequest { - private static final String AUTH_URL = "%s/auth/v1/login"; - private static final String USERNAME_FIELD_NAME = "username"; - private static final String PASSWORD_FIELD_NAME = "password"; - private String username; - private String password; - private String host; - - public RequestBody getRequestBody() throws JsonProcessingException { - Map loginDetailsMap = new HashMap<>(); - loginDetailsMap.put(USERNAME_FIELD_NAME, username); - loginDetailsMap.put(PASSWORD_FIELD_NAME, password); - return RequestBody.create(FireboltObjectMapper.getInstance().writeValueAsString(loginDetailsMap), - MediaType.parse("application/json")); - } + private static final String AUTH_URL = "%s/auth/v1/login"; + private static final String USERNAME_FIELD_NAME = "username"; + private static final String PASSWORD_FIELD_NAME = "password"; + private static final MediaType JSON = MediaType.parse("application/json"); + private final String username; + private final String password; + private final String host; - @Override - public String getUri() { - return String.format(AUTH_URL, host); - } + public RequestBody getRequestBody() { + JSONObject json = new JSONObject(); + json.put(USERNAME_FIELD_NAME, username); + json.put(PASSWORD_FIELD_NAME, password); + return RequestBody.create(json.toString(), JSON); + } -} \ No newline at end of file + @Override + public String getUri() { + return format(AUTH_URL, host); + } +} diff --git a/src/main/java/com/firebolt/jdbc/client/authentication/response/FireboltAuthenticationResponse.java b/src/main/java/com/firebolt/jdbc/client/authentication/response/FireboltAuthenticationResponse.java deleted file mode 100644 index 82232d75..00000000 --- a/src/main/java/com/firebolt/jdbc/client/authentication/response/FireboltAuthenticationResponse.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.firebolt.jdbc.client.authentication.response; - -import com.fasterxml.jackson.annotation.JsonProperty; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; - -@Value -@AllArgsConstructor -@Builder -public class FireboltAuthenticationResponse { - - @JsonProperty("access_token") - String accessToken; - - @JsonProperty("refresh_token") - String refreshToken; - - @JsonProperty("expires_in") - long expiresIn; -} diff --git a/src/main/java/com/firebolt/jdbc/client/config/OkHttpClientCreator.java b/src/main/java/com/firebolt/jdbc/client/config/OkHttpClientCreator.java index 15ff5879..532475f0 100644 --- a/src/main/java/com/firebolt/jdbc/client/config/OkHttpClientCreator.java +++ b/src/main/java/com/firebolt/jdbc/client/config/OkHttpClientCreator.java @@ -1,10 +1,29 @@ package com.firebolt.jdbc.client.config; +import com.firebolt.jdbc.client.config.socket.FireboltSSLSocketFactory; +import com.firebolt.jdbc.client.config.socket.FireboltSocketFactory; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.UtilityClass; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.security.*; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -12,26 +31,10 @@ import java.util.Optional; import java.util.concurrent.TimeUnit; -import javax.net.ssl.*; - -import org.apache.commons.lang3.StringUtils; - -import com.firebolt.jdbc.client.config.socket.FireboltSSLSocketFactory; -import com.firebolt.jdbc.client.config.socket.FireboltSocketFactory; -import com.firebolt.jdbc.connection.settings.FireboltProperties; - -import lombok.Builder; -import lombok.CustomLog; -import lombok.Value; -import lombok.experimental.UtilityClass; -import okhttp3.ConnectionPool; -import okhttp3.OkHttpClient; - /** * Class to configure the http client using the session settings */ @UtilityClass -@CustomLog public class OkHttpClientCreator { private static final String SSL_STRICT_MODE = "strict"; @@ -90,7 +93,7 @@ private static Optional getHostnameVerifier(FireboltProperties } private static Optional getSSLConfig(FireboltProperties fireboltProperties) throws CertificateException, - NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { + NoSuchAlgorithmException, KeyStoreException, IOException { if (!fireboltProperties.isSsl()) { return Optional.empty(); } @@ -123,34 +126,31 @@ private static Optional getSSLConfig(FireboltProperties fireboltPrope private static Optional getKeyStore(FireboltProperties fireboltProperties) throws NoSuchAlgorithmException, IOException, CertificateException, KeyStoreException { - if (StringUtils.isNotEmpty(fireboltProperties.getSslCertificatePath())) { - KeyStore keyStore; - keyStore = KeyStore.getInstance(JKS_KEYSTORE_TYPE); - try (InputStream certificate = openSslFile(fireboltProperties)) { - keyStore.load(null, null); - CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE_X_509); - int i = 0; - for (Certificate value : cf.generateCertificates(certificate)) { - keyStore.setCertificateEntry(String.format("Certificate_ %d)", i++), value); - } - return Optional.of(keyStore); - } - } else { + String sslCertificatePath = fireboltProperties.getSslCertificatePath(); + if (sslCertificatePath == null || sslCertificatePath.isEmpty()) { return Optional.empty(); } - + KeyStore keyStore = KeyStore.getInstance(JKS_KEYSTORE_TYPE); + try (InputStream certificate = openSslFile(sslCertificatePath)) { + keyStore.load(null, null); + CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE_X_509); + int i = 0; + for (Certificate value : cf.generateCertificates(certificate)) { + keyStore.setCertificateEntry(String.format("Certificate_ %d)", i++), value); + } + return Optional.of(keyStore); + } } - private static InputStream openSslFile(FireboltProperties fireboltProperties) throws IOException { + private static InputStream openSslFile(String sslCertificatePath) throws IOException { InputStream caInputStream; try { - caInputStream = new FileInputStream(fireboltProperties.getSslCertificatePath()); + caInputStream = new FileInputStream(sslCertificatePath); } catch (FileNotFoundException ex) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); - caInputStream = cl.getResourceAsStream(fireboltProperties.getSslCertificatePath()); + caInputStream = cl.getResourceAsStream(sslCertificatePath); if (caInputStream == null) { - throw new IOException(String.format("Could not open SSL/TLS certificate file %s", - fireboltProperties.getSslCertificatePath()), ex); + throw new IOException(String.format("Could not open SSL/TLS certificate file %s", sslCertificatePath), ex); } } return caInputStream; diff --git a/src/main/java/com/firebolt/jdbc/client/config/RetryInterceptor.java b/src/main/java/com/firebolt/jdbc/client/config/RetryInterceptor.java index 91667e4e..8959d568 100644 --- a/src/main/java/com/firebolt/jdbc/client/config/RetryInterceptor.java +++ b/src/main/java/com/firebolt/jdbc/client/config/RetryInterceptor.java @@ -1,27 +1,29 @@ package com.firebolt.jdbc.client.config; -import static java.net.HttpURLConnection.*; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; import java.io.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; -import lombok.NonNull; -import org.apache.commons.lang3.StringUtils; - -import lombok.CustomLog; -import lombok.RequiredArgsConstructor; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; +import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; +import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; @RequiredArgsConstructor -@CustomLog public class RetryInterceptor implements Interceptor { private static final Set RETRYABLE_RESPONSE_CODES = new HashSet<>( Arrays.asList(HTTP_CLIENT_TIMEOUT, HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT)); + private static final Logger log = Logger.getLogger(RetryInterceptor.class.getName()); private final int maxRetries; @@ -34,15 +36,16 @@ public Response intercept(@NonNull Chain chain) throws IOException { while (!response.isSuccessful() && RETRYABLE_RESPONSE_CODES.contains(response.code()) && tryCount++ < maxRetries) { String failureInfo; - if (request.tag() instanceof String && StringUtils.isNotEmpty((String) request.tag())) { + String tag = request.tag(String.class); + if (tag != null && !tag.isEmpty()) { failureInfo = String.format( "Failure #%d for query with id %s - Response code: %d. Retrying to send the request.", tryCount, - request.tag(), response.code()); + tag, response.code()); } else { failureInfo = String.format("Failure #%d - Response code: %d. Retrying to send the request.", tryCount, response.code()); } - log.warn(failureInfo); + log.log(Level.WARNING, failureInfo); // retry the request response.close(); diff --git a/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSSLSocketFactory.java b/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSSLSocketFactory.java index 5a6fe7e4..c1f3b06a 100644 --- a/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSSLSocketFactory.java +++ b/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSSLSocketFactory.java @@ -1,18 +1,14 @@ package com.firebolt.jdbc.client.config.socket; -import static com.firebolt.jdbc.client.config.socket.SocketUtil.wrap; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import javax.net.ssl.SSLSocketFactory; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import javax.net.ssl.SSLSocketFactory; - -import com.firebolt.jdbc.connection.settings.FireboltProperties; - -import lombok.CustomLog; +import static com.firebolt.jdbc.client.config.socket.SocketUtil.wrap; -@CustomLog public class FireboltSSLSocketFactory extends SSLSocketFactory { private final SSLSocketFactory delegate; private final FireboltProperties fireboltProperties; diff --git a/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSocketFactory.java b/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSocketFactory.java index 299715bd..e424c41b 100644 --- a/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSocketFactory.java +++ b/src/main/java/com/firebolt/jdbc/client/config/socket/FireboltSocketFactory.java @@ -1,18 +1,14 @@ package com.firebolt.jdbc.client.config.socket; -import static com.firebolt.jdbc.client.config.socket.SocketUtil.wrap; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import javax.net.SocketFactory; import java.io.IOException; import java.net.InetAddress; import java.net.Socket; -import javax.net.SocketFactory; - -import com.firebolt.jdbc.connection.settings.FireboltProperties; - -import lombok.CustomLog; +import static com.firebolt.jdbc.client.config.socket.SocketUtil.wrap; -@CustomLog public class FireboltSocketFactory extends SocketFactory { private static final javax.net.SocketFactory delegate = SocketFactory.getDefault(); private final FireboltProperties fireboltProperties; diff --git a/src/main/java/com/firebolt/jdbc/client/config/socket/SocketUtil.java b/src/main/java/com/firebolt/jdbc/client/config/socket/SocketUtil.java index cd70d43a..61b080b2 100644 --- a/src/main/java/com/firebolt/jdbc/client/config/socket/SocketUtil.java +++ b/src/main/java/com/firebolt/jdbc/client/config/socket/SocketUtil.java @@ -1,19 +1,19 @@ package com.firebolt.jdbc.client.config.socket; -import java.io.IOException; -import java.net.Socket; -import java.net.SocketOption; - import com.firebolt.jdbc.connection.settings.FireboltProperties; - import jdk.net.ExtendedSocketOptions; import jdk.net.Sockets; -import lombok.CustomLog; import lombok.experimental.UtilityClass; +import java.io.IOException; +import java.net.Socket; +import java.net.SocketOption; +import java.util.logging.Level; +import java.util.logging.Logger; + @UtilityClass -@CustomLog public class SocketUtil { + private static final Logger log = Logger.getLogger(SocketUtil.class.getName()); public static Socket wrap(Socket s, FireboltProperties fireboltProperties) throws IOException { s.setKeepAlive(true); @@ -23,7 +23,7 @@ public static Socket wrap(Socket s, FireboltProperties fireboltProperties) throw setSocketOption(s, ExtendedSocketOptions.TCP_KEEPCOUNT, fireboltProperties.getTcpKeepCount()); setSocketOption(s, ExtendedSocketOptions.TCP_KEEPINTERVAL, fireboltProperties.getTcpKeepInterval()); } catch (Error | Exception e) { - log.debug("Could not set socket options", e); + log.log(Level.FINE, "Could not set socket options", e); } return s; } @@ -32,8 +32,7 @@ private void setSocketOption(Socket socket, SocketOption option, int va try { Sockets.setOption(socket, option, value); } catch (Exception e) { - log.debug("Could not set the socket option {}. The operation is not supported: {}", option.name(), - e.getMessage()); + log.log(Level.FINE, "Could not set the socket option {0}. The operation is not supported: {1}", new Object[] {option.name(), e.getMessage()}); } } } diff --git a/src/main/java/com/firebolt/jdbc/client/gateway/GatewayUrlResponse.java b/src/main/java/com/firebolt/jdbc/client/gateway/GatewayUrlResponse.java new file mode 100644 index 00000000..d07ced33 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/client/gateway/GatewayUrlResponse.java @@ -0,0 +1,35 @@ +package com.firebolt.jdbc.client.gateway; + +import org.json.JSONObject; + +import java.util.Objects; + + +public class GatewayUrlResponse { + private final String engineUrl; + + public GatewayUrlResponse(String engineUrl) { + this.engineUrl = engineUrl; + } + + GatewayUrlResponse(JSONObject json) { + this(json.getString("engineUrl")); + } + + public String getEngineUrl() { + return engineUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + GatewayUrlResponse that = (GatewayUrlResponse) o; + return Objects.equals(engineUrl, that.engineUrl); + } + + @Override + public int hashCode() { + return Objects.hash(engineUrl); + } +} diff --git a/src/main/java/com/firebolt/jdbc/client/query/StatementClient.java b/src/main/java/com/firebolt/jdbc/client/query/StatementClient.java index c31b90ff..07d4c500 100644 --- a/src/main/java/com/firebolt/jdbc/client/query/StatementClient.java +++ b/src/main/java/com/firebolt/jdbc/client/query/StatementClient.java @@ -1,28 +1,23 @@ package com.firebolt.jdbc.client.query; -import java.io.InputStream; - import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.statement.StatementInfoWrapper; +import java.io.InputStream; +import java.sql.SQLException; + public interface StatementClient { /** * Post SQL statement */ InputStream executeSqlStatement(StatementInfoWrapper statementInfoWrapper, FireboltProperties connectionProperties, - boolean systemEngine, int queryTimeout, boolean standardSql) throws FireboltException; + boolean systemEngine, int queryTimeout) throws SQLException; /** * Call endpoint to abort a running SQL statement */ - void abortStatement(String id, FireboltProperties fireboltProperties) throws FireboltException; - - /** - * Abort running HTTP request of a statement - */ - void abortRunningHttpRequest(String id) throws FireboltException; + void abortStatement(String label, FireboltProperties fireboltProperties) throws SQLException; - boolean isStatementRunning(String statementId); + boolean isStatementRunning(String statementLabel); } diff --git a/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java b/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java index 8ef8f1ef..61d22e9f 100644 --- a/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java +++ b/src/main/java/com/firebolt/jdbc/client/query/StatementClientImpl.java @@ -1,6 +1,5 @@ package com.firebolt.jdbc.client.query; -import com.fasterxml.jackson.databind.ObjectMapper; import com.firebolt.jdbc.client.FireboltClient; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; @@ -12,44 +11,117 @@ import com.firebolt.jdbc.statement.rawstatement.RawStatement; import com.firebolt.jdbc.util.CloseableUtil; import com.firebolt.jdbc.util.PropertyUtil; -import lombok.CustomLog; import lombok.NonNull; import okhttp3.Call; +import okhttp3.Dispatcher; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.internal.http2.StreamResetException; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.DEFAULT_FORMAT; import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.OUTPUT_FORMAT; +import static com.firebolt.jdbc.exception.ExceptionType.INVALID_REQUEST; import static com.firebolt.jdbc.exception.ExceptionType.UNAUTHORIZED; +import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static java.util.Optional.ofNullable; -@CustomLog public class StatementClientImpl extends FireboltClient implements StatementClient { private static final String TAB_SEPARATED_WITH_NAMES_AND_TYPES_FORMAT = "TabSeparatedWithNamesAndTypes"; + private static final Map missConfigurationErrorMessages = Map.of( + Pattern.compile("HTTP status code: 401"), "Please associate user with your service account.", + Pattern.compile("Engine .+? does not exist or not authorized"), "Please grant at least one role to user associated your service account." + ); + private static final Logger log = Logger.getLogger(StatementClientImpl.class.getName()); - private final BiPredicate isCallWithId = (call, id) -> call.request().tag() instanceof String - && StringUtils.equals((String) call.request().tag(), id); + private final BiPredicate isCallWithLabel = (call, label) -> call.request().tag() instanceof String && Objects.equals(call.request().tag(), label); + // visible for testing + static final String HEADER_UPDATE_PARAMETER = "Firebolt-Update-Parameters"; + static final String HEADER_UPDATE_ENDPOINT = "Firebolt-Update-Endpoint"; + static final String HEADER_RESET_SESSION = "Firebolt-Reset-Session"; - public StatementClientImpl(OkHttpClient httpClient, FireboltConnection connection, ObjectMapper objectMapper, - String customDrivers, String customClients) { - super(httpClient, connection, customDrivers, customClients, objectMapper); + private enum QueryIdFetcher { + /** + * Attach label to statement using trailing comment. This is a hack because label cannot be normally attached to + * statement in old version + */ + COMMENT { + @Override + String formatStatement(StatementInfoWrapper statementInfoWrapper) { + return QUERY_LABEL.formatStatement(statementInfoWrapper) + "--label:" + statementInfoWrapper.getLabel(); + } + + @Override + String queryIdFetcher() { + return "select query_id from information_schema.query_history where status = 'STARTED_EXECUTION' and query_text like ?"; + } + + @Override + String queryIdLabel(String label) { + return "%label:" + label; + } + }, + /** + * Attach label to query using special request parameters {@code query_label} + */ + QUERY_LABEL { + @Override + String formatStatement(StatementInfoWrapper statementInfoWrapper) { + return ofNullable(statementInfoWrapper.getInitialStatement()).map(RawStatement::getCleanSql) + .filter(cleanSql -> !cleanSql.endsWith(";")) + .map(cleanSql -> statementInfoWrapper.getSql() + ";") + .orElse(statementInfoWrapper.getSql()); + } + + @Override + String queryIdFetcher() { + return "select query_id from information_schema.engine_query_history where status = 'STARTED_EXECUTION' and query_label = ?"; + } + + @Override + String queryIdLabel(String label) { + return label; + } + }, + ; + + abstract String formatStatement(StatementInfoWrapper statementInfoWrapper); + abstract String queryIdFetcher(); + abstract String queryIdLabel(String label); + + static QueryIdFetcher getQueryFetcher(int infraVersion) { + return infraVersion < 2 ? COMMENT : QUERY_LABEL; + } + + } + + public StatementClientImpl(OkHttpClient httpClient, FireboltConnection connection, String customDrivers, String customClients) { + super(httpClient, connection, customDrivers, customClients); } /** @@ -60,122 +132,147 @@ public StatementClientImpl(OkHttpClient httpClient, FireboltConnection connectio * @param connectionProperties the connection properties * @param systemEngine indicates if system engine is used * @param queryTimeout query timeout - * @param standardSql indicates if standard sql should be used * @return the server response */ @Override public InputStream executeSqlStatement(@NonNull StatementInfoWrapper statementInfoWrapper, - @NonNull FireboltProperties connectionProperties, boolean systemEngine, int queryTimeout, - boolean standardSql) throws FireboltException { - String formattedStatement = formatStatement(statementInfoWrapper); + @NonNull FireboltProperties connectionProperties, boolean systemEngine, int queryTimeout) throws SQLException { + QueryIdFetcher.getQueryFetcher(connection.getInfraVersion()).formatStatement(statementInfoWrapper); + String formattedStatement = QueryIdFetcher.getQueryFetcher(connection.getInfraVersion()).formatStatement(statementInfoWrapper); Map params = getAllParameters(connectionProperties, statementInfoWrapper, systemEngine, queryTimeout); + String label = statementInfoWrapper.getLabel(); + String errorMessage = format("Error executing statement with label %s: %s", label, formattedStatement); try { - String uri = this.buildQueryUri(connectionProperties, params).toString(); - return executeSqlStatementWithRetryOnUnauthorized(statementInfoWrapper, connectionProperties, - formattedStatement, uri); + String uri = buildQueryUri(connectionProperties, params).toString(); + return executeSqlStatementWithRetryOnUnauthorized(label, connectionProperties, formattedStatement, uri); } catch (FireboltException e) { throw e; + } catch (StreamResetException e) { + throw new FireboltException(errorMessage, e, ExceptionType.CANCELED); } catch (Exception e) { - String errorMessage = String.format("Error executing statement with id %s: %s", - statementInfoWrapper.getId(), formattedStatement); - if (e instanceof StreamResetException) { - throw new FireboltException(errorMessage, e, ExceptionType.CANCELED); - } throw new FireboltException(errorMessage, e); } - } - private InputStream executeSqlStatementWithRetryOnUnauthorized(@NonNull StatementInfoWrapper statementInfoWrapper, - @NonNull FireboltProperties connectionProperties, String formattedStatement, String uri) - throws IOException, FireboltException { + private InputStream executeSqlStatementWithRetryOnUnauthorized(String label, @NonNull FireboltProperties connectionProperties, String formattedStatement, String uri) + throws SQLException, IOException { try { - log.debug("Posting statement with id {} to URI: {}", statementInfoWrapper.getId(), uri); - return postSqlStatement(statementInfoWrapper, connectionProperties, formattedStatement, uri); + log.log(Level.FINE, "Posting statement with label {0} to URI: {1}", new Object[] {label, uri}); + return postSqlStatement(connectionProperties, formattedStatement, uri, label); } catch (FireboltException exception) { if (exception.getType() == UNAUTHORIZED) { - log.debug("Retrying to post statement with id {} following a 401 status code to URI: {}", - statementInfoWrapper.getId(), uri); - return postSqlStatement(statementInfoWrapper, connectionProperties, formattedStatement, uri); + log.log(Level.FINE, "Retrying to post statement with label {0} following a 401 status code to URI: {1}", new Object[] {label, uri}); + return postSqlStatement(connectionProperties, formattedStatement, uri, label); } else { throw exception; } } } - private InputStream postSqlStatement(@NonNull StatementInfoWrapper statementInfoWrapper, - @NonNull FireboltProperties connectionProperties, String formattedStatement, String uri) - throws FireboltException, IOException { - Response response; - Request post = this.createPostRequest(uri, formattedStatement, - this.getConnection().getAccessToken().orElse(null), statementInfoWrapper.getId()); - response = this.execute(post, connectionProperties.getHost(), connectionProperties.isCompress()); - InputStream is = Optional.ofNullable(response.body()).map(ResponseBody::byteStream).orElse(null); + private InputStream postSqlStatement(@NonNull FireboltProperties connectionProperties, String formattedStatement, String uri, String label) + throws SQLException, IOException { + Request post = createPostRequest(uri, label, formattedStatement, getConnection().getAccessToken().orElse(null)); + Response response = execute(post, connectionProperties.getHost(), connectionProperties.isCompress()); + InputStream is = ofNullable(response.body()).map(ResponseBody::byteStream).orElse(null); if (is == null) { CloseableUtil.close(response); } return is; } - private String formatStatement(StatementInfoWrapper statementInfoWrapper) { - Optional cleanSql = Optional.ofNullable(statementInfoWrapper.getInitialStatement()) - .map(RawStatement::getCleanSql); - if (cleanSql.isPresent() && !StringUtils.endsWith(cleanSql.get(), ";")) { - return statementInfoWrapper.getSql() + ";"; + public void abortStatement(@NonNull String statementLabel, @NonNull FireboltProperties properties) throws SQLException { + boolean aborted = abortRunningHttpRequest(statementLabel); + if (properties.isSystemEngine()) { + throw new FireboltException("Cannot cancel a statement using a system engine", INVALID_REQUEST); } else { - return statementInfoWrapper.getSql(); + abortRunningDbStatement(statementLabel, properties, aborted ? 10_000 : 1); } } /** * Aborts the statement being sent to the server * - * @param id id of the statement + * @param label label of the statement * @param fireboltProperties the properties */ - public void abortStatement(String id, FireboltProperties fireboltProperties) throws FireboltException { + private void abortRunningDbStatement(String label, FireboltProperties fireboltProperties, int getIdTimeout) throws SQLException { try { - String uri = this.buildCancelUri(fireboltProperties, id).toString(); - Request rq = this.createPostRequest(uri, this.getConnection().getAccessToken().orElse(null), null); - try (Response response = this.execute(rq, fireboltProperties.getHost())) { + String id; + int attempt = 0; + int getIdAttempts = 10; + int getIdDelay = Math.max(getIdTimeout / getIdAttempts, 1); + // Statement ID is retrieved from query_history table. Records are written to this table asynchronously. + // So, if cancel() is called immediately after executing the statement sometimes the record in query_history + // can be unavailable. To retrieve it we perform several attempts. + for (id = getStatementId(label); attempt < getIdAttempts; id = getStatementId(label), attempt++) { + if (id != null) { + break; + } + delay(getIdDelay); + } + if (id == null) { + throw new FireboltException("Cannot retrieve id for statement with label " + label); + } + String uri = buildCancelUri(fireboltProperties, id).toString(); + Request rq = createPostRequest(uri, null, (RequestBody)null, getConnection().getAccessToken().orElse(null)); + try (Response response = execute(rq, fireboltProperties.getHost())) { CloseableUtil.close(response); } } catch (FireboltException e) { - if (e.getType() == ExceptionType.INVALID_REQUEST) { + if (e.getType() == ExceptionType.INVALID_REQUEST || e.getType() == ExceptionType.RESOURCE_NOT_FOUND) { // 400 on that request indicates that the statement does not exist - log.warn(e.getMessage()); + // 404 - the same when working against "real" v2 engine + log.warning(e.getMessage()); } else { throw e; } } catch (Exception e) { - throw new FireboltException( - String.format("Could not cancel query: %s at %s", id, fireboltProperties.getHost()), e); + throw new FireboltException(format("Could not cancel query: %s at %s", label, fireboltProperties.getHost()), e); + } + } + + @SuppressWarnings("java:S2142") // "InterruptedException" and "ThreadDeath" should not be ignored + private void delay(int delay) { + try { + Thread.sleep(delay); + } catch (InterruptedException e) { + // ignore interrupted exception } } /** * Abort HttpRequest if it is currently being sent * - * @param id id of the statement + * @param label id of the statement */ - public void abortRunningHttpRequest(@NonNull String id) { - getQueuedCallWithId(id).ifPresent(Call::cancel); - getRunningCallWithId(id).ifPresent(Call::cancel); + private boolean abortRunningHttpRequest(@NonNull String label) { + boolean quedAborted = abortCall(getQueuedCallWithLabel(label)); + boolean runningAborted = abortCall(getRunningCallWithLabel(label)); + return quedAborted || runningAborted; } - private Optional getQueuedCallWithId(String id) { - return getHttpClient().dispatcher().queuedCalls().stream().filter(call -> isCallWithId.test(call, id)) - .findAny(); + private boolean abortCall(Optional call) { + return call.map(c -> { + c.cancel(); + return true; + }).orElse(false); } - private Optional getRunningCallWithId(String id) { - return getHttpClient().dispatcher().runningCalls().stream().filter(call -> isCallWithId.test(call, id)) - .findAny(); + private Optional getQueuedCallWithLabel(String label) { + return getSelectedCallWithLabel(label, Dispatcher::queuedCalls); + } + + private Optional getRunningCallWithLabel(String id) { + return getSelectedCallWithLabel(id, Dispatcher::runningCalls); + } + + private Optional getSelectedCallWithLabel(String label, Function> callsGetter) { + return callsGetter.apply(getHttpClient().dispatcher()).stream().filter(call -> isCallWithLabel.test(call, label)).findAny(); } @Override public boolean isStatementRunning(String statementId) { - return getQueuedCallWithId(statementId).isPresent() || getRunningCallWithId(statementId).isPresent(); + return getQueuedCallWithLabel(statementId).isPresent() || getRunningCallWithLabel(statementId).isPresent(); } private URI buildQueryUri(FireboltProperties fireboltProperties, Map parameters) { @@ -190,8 +287,9 @@ private URI buildCancelUri(FireboltProperties fireboltProperties, String id) { private URI buildURI(FireboltProperties fireboltProperties, Map parameters, List pathSegments) { HttpUrl.Builder httpUrlBuilder = new HttpUrl.Builder() - .scheme(Boolean.TRUE.equals(fireboltProperties.isSsl()) ? "https" : "http") - .host(fireboltProperties.getHost()).port(fireboltProperties.getPort()); + .scheme(fireboltProperties.isSsl() ? "https" : "http") + .host(fireboltProperties.getHost()) + .port(fireboltProperties.getPort()); parameters.forEach(httpUrlBuilder::addQueryParameter); pathSegments.forEach(httpUrlBuilder::addPathSegment); @@ -206,32 +304,82 @@ private Map getAllParameters(FireboltProperties fireboltProperti Map params = new HashMap<>(fireboltProperties.getAdditionalProperties()); getResponseFormatParameter(statementInfoWrapper.getType() == StatementType.QUERY, isLocalDb) - .ifPresent(format -> params.put(format.getLeft(), format.getRight())); - // System engines do not support the following query params - if (!systemEngine) { - params.put(FireboltQueryParameterKey.DATABASE.getKey(), fireboltProperties.getDatabase()); - params.put(FireboltQueryParameterKey.QUERY_ID.getKey(), statementInfoWrapper.getId()); - params.put(FireboltQueryParameterKey.COMPRESS.getKey(), - String.format("%d", fireboltProperties.isCompress() ? 1 : 0)); - - if (queryTimeout > -1) { + .ifPresent(format -> params.put(format.getKey(), format.getValue())); + + String accountId = fireboltProperties.getAccountId(); + if (systemEngine) { + if (accountId != null && connection.getInfraVersion() < 2) { + // if infra version >= 2 we should add account_id only if it was supplied by system URL returned from server. + // In this case it will be in additionalProperties anyway. + params.put(FireboltQueryParameterKey.ACCOUNT_ID.getKey(), accountId); + } + } else { + if (connection.getInfraVersion() >= 2) { + if (accountId != null) { + params.put(FireboltQueryParameterKey.ACCOUNT_ID.getKey(), accountId); + params.put(FireboltQueryParameterKey.ENGINE.getKey(), fireboltProperties.getEngine()); + } + params.put(FireboltQueryParameterKey.QUERY_LABEL.getKey(), statementInfoWrapper.getLabel()); //QUERY_LABEL + } + params.put(FireboltQueryParameterKey.COMPRESS.getKey(), fireboltProperties.isCompress() ? "1" : "0"); + + if (queryTimeout > 0) { params.put("max_execution_time", String.valueOf(queryTimeout)); } } + params.put(FireboltQueryParameterKey.DATABASE.getKey(), fireboltProperties.getDatabase()); return params; } - private Optional> getResponseFormatParameter(boolean isQuery, boolean isLocalDb) { - if (isQuery) { - FireboltQueryParameterKey key = isLocalDb ? DEFAULT_FORMAT : OUTPUT_FORMAT; - return Optional.of(new ImmutablePair<>(key.getKey(), TAB_SEPARATED_WITH_NAMES_AND_TYPES_FORMAT)); - } - return Optional.empty(); + private Optional> getResponseFormatParameter(boolean isQuery, boolean isLocalDb) { + FireboltQueryParameterKey format = isLocalDb ? DEFAULT_FORMAT : OUTPUT_FORMAT; + return isQuery ? Optional.of(Map.entry(format.getKey(), TAB_SEPARATED_WITH_NAMES_AND_TYPES_FORMAT)) : Optional.empty(); } private Map getCancelParameters(String statementId) { return Map.of(FireboltQueryParameterKey.QUERY_ID.getKey(), statementId); } + @Override + protected void validateResponse(String host, Response response, Boolean isCompress) throws SQLException { + super.validateResponse(host, response, isCompress); + FireboltConnection connection = getConnection(); + if (isCallSuccessful(response.code())) { + if (response.header(HEADER_RESET_SESSION) != null) { + connection.reset(); + } + String endpoint = response.header(HEADER_UPDATE_ENDPOINT); + if (endpoint != null) { + connection.setEndpoint(connection.getSessionProperties().processEngineUrl(endpoint)); + } + for (String header : response.headers(HEADER_UPDATE_PARAMETER)) { + String[] keyValue = header.split("="); + connection.addProperty(keyValue[0].trim(), keyValue[1].trim()); + } + } + } + + @Override + protected void validateResponse(String host, int statusCode, String errorMessageFromServer) throws SQLException { + if (statusCode == HTTP_INTERNAL_ERROR) { + FireboltException ex = missConfigurationErrorMessages.entrySet().stream() + .filter(msg -> msg.getKey().matcher(errorMessageFromServer).find()).findFirst() + .map(msg -> new FireboltException(format("Could not query Firebolt at %s. %s", host, msg.getValue()), HTTP_UNAUTHORIZED, errorMessageFromServer)) + .orElse(null); + if (ex != null) { + throw ex; + } + } + } + + private String getStatementId(String label) throws SQLException { + QueryIdFetcher queryIdFetcher = QueryIdFetcher.getQueryFetcher(connection.getInfraVersion()); + try (PreparedStatement ps = connection.prepareStatement(queryIdFetcher.queryIdFetcher())) { + ps.setString(1, queryIdFetcher.queryIdLabel(label)); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString(1) : null; + } + } + } } diff --git a/src/main/java/com/firebolt/jdbc/connection/CacheListener.java b/src/main/java/com/firebolt/jdbc/connection/CacheListener.java new file mode 100644 index 00000000..7090712a --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/CacheListener.java @@ -0,0 +1,7 @@ +package com.firebolt.jdbc.connection; + +import java.util.EventListener; + +public interface CacheListener extends EventListener { + void cleanup(); +} diff --git a/src/main/java/com/firebolt/jdbc/connection/Engine.java b/src/main/java/com/firebolt/jdbc/connection/Engine.java index d00040f5..17b80429 100644 --- a/src/main/java/com/firebolt/jdbc/connection/Engine.java +++ b/src/main/java/com/firebolt/jdbc/connection/Engine.java @@ -1,13 +1,14 @@ package com.firebolt.jdbc.connection; -import lombok.Builder; -import lombok.Data; +import lombok.*; -@Builder +@AllArgsConstructor @Data +@EqualsAndHashCode public class Engine { - private String endpoint; - private String id; - private String status; - private String name; + private final String endpoint; + private final String status; + private final String name; + private final String database; + private final String id; } diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java index 010554df..e010c728 100644 --- a/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnection.java @@ -1,39 +1,32 @@ package com.firebolt.jdbc.connection; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.JdbcBase; import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; import com.firebolt.jdbc.annotation.NotImplemented; -import com.firebolt.jdbc.client.FireboltObjectMapper; import com.firebolt.jdbc.client.HttpClientConfig; -import com.firebolt.jdbc.client.account.FireboltAccountClient; import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; import com.firebolt.jdbc.client.query.StatementClientImpl; import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException; -import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException; import com.firebolt.jdbc.metadata.FireboltDatabaseMetadata; import com.firebolt.jdbc.metadata.FireboltSystemEngineDatabaseMetadata; import com.firebolt.jdbc.service.FireboltAuthenticationService; -import com.firebolt.jdbc.service.FireboltEngineService; import com.firebolt.jdbc.service.FireboltStatementService; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.statement.preparedstatement.FireboltPreparedStatement; import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.type.array.FireboltArray; +import com.firebolt.jdbc.type.lob.FireboltBlob; +import com.firebolt.jdbc.type.lob.FireboltClob; import com.firebolt.jdbc.util.PropertyUtil; -import lombok.CustomLog; import lombok.NonNull; import okhttp3.OkHttpClient; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; import java.io.IOException; -import java.security.KeyManagementException; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; +import java.security.GeneralSecurityException; import java.sql.Array; import java.sql.Blob; import java.sql.CallableStatement; @@ -52,102 +45,152 @@ import java.sql.Statement; import java.sql.Struct; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; import java.util.concurrent.Executor; - +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.getNonDeprecatedProperties; +import static java.lang.String.format; import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; +import static java.util.stream.Collectors.toMap; -@CustomLog -public class FireboltConnection implements Connection { +public abstract class FireboltConnection extends JdbcBase implements Connection, CacheListener { + private static final Logger log = Logger.getLogger(FireboltConnection.class.getName()); private final FireboltAuthenticationService fireboltAuthenticationService; - private final FireboltEngineService fireboltEngineService; private final FireboltStatementService fireboltStatementService; - private final String httpConnectionUrl; + protected String httpConnectionUrl; private final List statements; private final int connectionTimeout; - private final boolean systemEngine; private boolean closed = true; - private FireboltProperties sessionProperties; + protected FireboltProperties sessionProperties; private int networkTimeout; + private final String protocolVersion; + protected int infraVersion = 1; + private DatabaseMetaData databaseMetaData; //Properties that are used at the beginning of the connection for authentication - private final FireboltProperties loginProperties; + protected final FireboltProperties loginProperties; + private final Collection cacheListeners = Collections.newSetFromMap(new IdentityHashMap<>()); + + protected FireboltConnection(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltStatementService fireboltStatementService, + String protocolVersion) { + this.loginProperties = extractFireboltProperties(url, connectionSettings); - public FireboltConnection(@NonNull String url, Properties connectionSettings, - FireboltAuthenticationService fireboltAuthenticationService, FireboltEngineService fireboltEngineService, - FireboltStatementService fireboltStatementService) throws FireboltException { this.fireboltAuthenticationService = fireboltAuthenticationService; - this.fireboltEngineService = fireboltEngineService; - loginProperties = this.extractFireboltProperties(url, connectionSettings); - this.httpConnectionUrl = getHttpConnectionUrl(loginProperties); + this.httpConnectionUrl = loginProperties.getHttpConnectionUrl(); this.fireboltStatementService = fireboltStatementService; + this.statements = new ArrayList<>(); this.connectionTimeout = loginProperties.getConnectionTimeoutMillis(); this.networkTimeout = loginProperties.getSocketTimeoutMillis(); - this.systemEngine = loginProperties.isSystemEngine(); - this.connect(); + this.protocolVersion = protocolVersion; } + // This code duplication between constructors is done because of back reference: dependent services require reference to current instance of FireboltConnection that prevents using constructor chaining or factory method. @ExcludeFromJacocoGeneratedReport - public FireboltConnection(@NonNull String url, Properties connectionSettings) throws FireboltException { - ObjectMapper objectMapper = FireboltObjectMapper.getInstance(); - loginProperties = this.extractFireboltProperties(url, connectionSettings); - this.httpConnectionUrl = getHttpConnectionUrl(loginProperties); + protected FireboltConnection(@NonNull String url, Properties connectionSettings, String protocolVersion) throws SQLException { + this.loginProperties = extractFireboltProperties(url, connectionSettings); OkHttpClient httpClient = getHttpClient(loginProperties); - this.systemEngine = loginProperties.isSystemEngine(); - this.fireboltAuthenticationService = new FireboltAuthenticationService( - new FireboltAuthenticationClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); - this.fireboltEngineService = new FireboltEngineService( - new FireboltAccountClient(httpClient, objectMapper, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); - this.fireboltStatementService = new FireboltStatementService( - new StatementClientImpl(httpClient, this, objectMapper, loginProperties.getUserDrivers(), loginProperties.getUserClients()), systemEngine); + + this.fireboltAuthenticationService = new FireboltAuthenticationService(createFireboltAuthenticationClient(httpClient)); + this.httpConnectionUrl = loginProperties.getHttpConnectionUrl(); + this.fireboltStatementService = new FireboltStatementService(new StatementClientImpl(httpClient, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); + this.statements = new ArrayList<>(); this.connectionTimeout = loginProperties.getConnectionTimeoutMillis(); this.networkTimeout = loginProperties.getSocketTimeoutMillis(); - this.connect(); + this.protocolVersion = protocolVersion; + } + + protected abstract FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient); + + public static FireboltConnection create(@NonNull String url, Properties connectionSettings) throws SQLException { + return createConnectionInstance(url, connectionSettings); + } + + private static FireboltConnection createConnectionInstance(@NonNull String url, Properties connectionSettings) throws SQLException { + switch(getUrlVersion(url, connectionSettings)) { + case 1: return new FireboltConnectionUserPassword(url, connectionSettings); + case 2: return new FireboltConnectionServiceSecret(url, connectionSettings); + default: throw new IllegalArgumentException(format("Cannot distinguish version from url %s", url)); + } + } + + private static int getUrlVersion(String url, Properties connectionSettings) { + Pattern urlWithHost = Pattern.compile("jdbc:firebolt://api\\.\\w+\\.firebolt\\.io"); + if (!urlWithHost.matcher(url).find()) { + return 2; // new URL format + } + // old URL format + Properties propertiesFromUrl = UrlUtil.extractProperties(url); + Properties allSettings = PropertyUtil.mergeProperties(propertiesFromUrl, connectionSettings); + if (allSettings.containsKey("client_id") && allSettings.containsKey("client_secret") && !allSettings.containsKey("user") && !allSettings.containsKey("password")) { + return 2; + } + FireboltProperties props = new FireboltProperties(new Properties[] {propertiesFromUrl, connectionSettings}); + String principal = props.getPrincipal(); + if (props.getAccessToken() != null || (principal != null && principal.contains("@"))) { + return 1; + } + return 2; } - private static OkHttpClient getHttpClient(FireboltProperties fireboltProperties) throws FireboltException { + protected OkHttpClient getHttpClient(FireboltProperties fireboltProperties) throws SQLException { try { - return HttpClientConfig.getInstance() == null ? HttpClientConfig.init(fireboltProperties) - : HttpClientConfig.getInstance(); - } catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | KeyManagementException - | IOException e) { + return HttpClientConfig.getInstance() == null ? HttpClientConfig.init(fireboltProperties) : HttpClientConfig.getInstance(); + } catch (GeneralSecurityException | IOException e) { throw new FireboltException("Could not instantiate http client", e); } } - private void connect() throws FireboltException { - String accessToken = getAccessToken(loginProperties).orElse(StringUtils.EMPTY); + protected void connect() throws SQLException { + closed = false; if (!PropertyUtil.isLocalDb(loginProperties)) { - String endpoint = fireboltEngineService.getEngine(httpConnectionUrl, loginProperties, accessToken) - .getEndpoint(); - this.sessionProperties = loginProperties.toBuilder().host(endpoint).build(); + authenticate(); } else { - this.sessionProperties = loginProperties; + // When running packdb locally, the login properties are the session properties + sessionProperties = loginProperties; + // The validation of not local DB is implemented into authenticate() method itself. + assertDatabaseExisting(loginProperties.getDatabase()); } - closed = false; - log.debug("Connection opened"); + databaseMetaData = retrieveMetaData(); + + log.fine("Connection opened"); } - public void removeExpiredTokens() throws FireboltException { + protected abstract void authenticate() throws SQLException; + + protected abstract void assertDatabaseExisting(String database) throws SQLException; + + public void removeExpiredTokens() throws SQLException { fireboltAuthenticationService.removeConnectionTokens(httpConnectionUrl, loginProperties); } - public Optional getAccessToken() throws FireboltException { - return this.getAccessToken(sessionProperties); + public Optional getAccessToken() throws SQLException { + return getAccessToken(sessionProperties); } - private Optional getAccessToken(FireboltProperties fireboltProperties) throws FireboltException { + protected Optional getAccessToken(FireboltProperties fireboltProperties) throws SQLException { String accessToken = fireboltProperties.getAccessToken(); if (accessToken != null) { - if (fireboltProperties.getUser() != null || fireboltProperties.getPassword() != null) { - throw new FireboltException("Ambiguity: Both access token and user/password are supplied"); + if (fireboltProperties.getPrincipal() != null || fireboltProperties.getSecret() != null) { + throw new FireboltException("Ambiguity: Both access token and client ID/secret are supplied"); } return Optional.of(accessToken); } @@ -159,55 +202,58 @@ private Optional getAccessToken(FireboltProperties fireboltProperties) t } public FireboltProperties getSessionProperties() { - return this.sessionProperties; + return sessionProperties; } @Override public Statement createStatement() throws SQLException { - this.validateConnectionIsNotClose(); - return this.createStatement(this.getSessionProperties()); + validateConnectionIsNotClose(); + return createStatement(getSessionProperties()); } - public Statement createStatement(FireboltProperties fireboltProperties) throws SQLException { - this.validateConnectionIsNotClose(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(this).build(); - this.addStatement(fireboltStatement); + private Statement createStatement(FireboltProperties fireboltProperties) throws SQLException { + validateConnectionIsNotClose(); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, this); + addStatement(fireboltStatement); return fireboltStatement; } private void addStatement(FireboltStatement statement) throws SQLException { synchronized (statements) { - this.validateConnectionIsNotClose(); - this.statements.add(statement); + validateConnectionIsNotClose(); + statements.add(statement); } } @Override public boolean getAutoCommit() throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); return true; } @Override @ExcludeFromJacocoGeneratedReport @NotImplemented - public void setAutoCommit(boolean autoCommit) throws SQLException { + public void setAutoCommit(boolean autoCommit) { // No-op as Firebolt does not support transactions } @Override public boolean isClosed() { - return this.closed; + return closed; } @Override public DatabaseMetaData getMetaData() throws SQLException { - this.validateConnectionIsNotClose(); - if (!this.systemEngine) { - return new FireboltDatabaseMetadata(this.httpConnectionUrl, this); + validateConnectionIsNotClose(); + return databaseMetaData; + } + + private DatabaseMetaData retrieveMetaData() { + if (!loginProperties.isSystemEngine()) { + return new FireboltDatabaseMetadata(httpConnectionUrl, this); } else { - return new FireboltSystemEngineDatabaseMetadata(this.httpConnectionUrl, this); + return new FireboltSystemEngineDatabaseMetadata(httpConnectionUrl, this); } } @@ -219,31 +265,30 @@ public String getCatalog() throws SQLException { @Override @NotImplemented - public void setCatalog(String catalog) throws SQLException { + public void setCatalog(String catalog) { // no-op as catalogs are not supported } - public String getEngine() throws SQLException { - this.validateConnectionIsNotClose(); - return fireboltEngineService.getEngineNameFromHost(this.getSessionProperties().getHost()); + public String getEngine() { + return getSessionProperties().getEngine(); } @Override public int getTransactionIsolation() throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); return Connection.TRANSACTION_NONE; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public void setTransactionIsolation(int level) throws SQLException { - throw new FireboltUnsupportedOperationException(); + if (level != Connection.TRANSACTION_NONE) { + throw new FireboltSQLFeatureNotSupportedException(); + } } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); if (resultSetType != TYPE_FORWARD_ONLY || resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) { throw new FireboltSQLFeatureNotSupportedException(); } @@ -267,58 +312,57 @@ public void abort(Executor executor) throws SQLException { if (executor == null) { throw new FireboltException("Cannot abort: the executor is null"); } - if (!this.closed) { + if (!closed) { executor.execute(this::close); } } @Override public void close() { - log.debug("Closing connection"); + log.fine("Closing connection"); synchronized (this) { - if (this.isClosed()) { + if (isClosed()) { return; } else { closed = true; } } synchronized (statements) { - for (FireboltStatement statement : this.statements) { + for (FireboltStatement statement : statements) { try { statement.close(false); } catch (Exception e) { - log.warn("Could not close statement", e); + log.log(Level.WARNING, "Could not close statement", e); } } statements.clear(); } - log.debug("Connection closed"); + databaseMetaData = null; + log.warning("Connection closed"); } - private FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { - Properties propertiesFromUrl = FireboltJdbcUrlUtil.extractProperties(jdbcUri); - return FireboltProperties.of(propertiesFromUrl, connectionProperties); + protected FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { + return createFireboltProperties(jdbcUri, connectionProperties); } - private String getHttpConnectionUrl(FireboltProperties newSessionProperties) { - String hostAndPort = newSessionProperties.getHost() + ":" + newSessionProperties.getPort(); - return newSessionProperties.isSsl() ? "https://" + hostAndPort : "http://" + hostAndPort; + private static FireboltProperties createFireboltProperties(String jdbcUri, Properties connectionProperties) { + return new FireboltProperties(new Properties[] {UrlUtil.extractProperties(jdbcUri), connectionProperties}); } @Override public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { - return this.prepareStatement(sql, resultSetType, resultSetConcurrency); + return prepareStatement(sql, resultSetType, resultSetConcurrency); } @Override public PreparedStatement prepareStatement(String sql) throws SQLException { - return this.createPreparedStatement(sql); + return createPreparedStatement(sql); } @Override public CallableStatement prepareCall(String sql) throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); throw new FireboltSQLFeatureNotSupportedException(); } @@ -328,40 +372,41 @@ public PreparedStatement prepareStatement(String sql, int resultSetType, int res if (resultSetType != TYPE_FORWARD_ONLY) { throw new FireboltSQLFeatureNotSupportedException(); } - return this.prepareStatement(sql); + return prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { - this.validateConnectionIsNotClose(); - throw new FireboltSQLFeatureNotSupportedException(); + validateConnectionIsNotClose(); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new FireboltSQLFeatureNotSupportedException(); + } + return prepareStatement(sql); } @Override public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); throw new FireboltSQLFeatureNotSupportedException(); } @Override public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - this.validateConnectionIsNotClose(); + validateConnectionIsNotClose(); throw new FireboltSQLFeatureNotSupportedException(); } private PreparedStatement createPreparedStatement(String sql) throws SQLException { - this.validateConnectionIsNotClose(); - FireboltPreparedStatement statement = FireboltPreparedStatement.statementBuilder() - .statementService(fireboltStatementService).sessionProperties(this.getSessionProperties()).sql(sql) - .connection(this).build(); - this.addStatement(statement); + validateConnectionIsNotClose(); + FireboltPreparedStatement statement = new FireboltPreparedStatement(fireboltStatementService, this, sql); + addStatement(statement); return statement; } @Override public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { - return this.createStatement(resultSetType, resultSetConcurrency); + return createStatement(resultSetType, resultSetConcurrency); } public boolean isValid(int timeout) throws SQLException { @@ -372,8 +417,8 @@ public boolean isValid(int timeout) throws SQLException { return false; } try { - if (!this.systemEngine) { - validateConnection(this.getSessionProperties(), true); + if (!loginProperties.isSystemEngine()) { + validateConnection(getSessionProperties(), true); } return true; } catch (Exception e) { @@ -390,9 +435,9 @@ private void validateConnection(FireboltProperties fireboltProperties, boolean i // This error cannot be ignored when testing the connection to validate a param. if (ignoreToManyRequestsError && e instanceof FireboltException && ((FireboltException) e).getType() == ExceptionType.TOO_MANY_REQUESTS) { - log.warn("Too many requests are being sent to the server", e); + log.log(Level.WARNING, "Too many requests are being sent to the server", e); } else { - log.warn("Connection is not valid", e); + log.log(Level.WARNING, "Connection is not valid", e); throw e; } } @@ -406,27 +451,46 @@ private void validateConnectionIsNotClose() throws SQLException { public void removeClosedStatement(FireboltStatement fireboltStatement) { synchronized (statements) { - this.statements.remove(fireboltStatement); + statements.remove(fireboltStatement); } } - public synchronized void addProperty(Pair property) throws FireboltException { + public void addProperty(@NonNull String key, String value) throws SQLException { + changeProperty(p -> p.addProperty(key, value), () -> format("Could not set property %s=%s", key, value)); + } + + public void addProperty(Entry property) throws SQLException { + changeProperty(p -> p.addProperty(property), () -> format("Could not set property %s=%s", property.getKey(), property.getValue())); + } + + public void reset() throws SQLException { + changeProperty(FireboltProperties::clearAdditionalProperties, () -> "Could not reset connection"); + } + + private synchronized void changeProperty(Consumer propertiesEditor, Supplier errorMessageFactory) throws SQLException { try { - FireboltProperties tmpProperties = FireboltProperties.copy(this.sessionProperties); - tmpProperties.addProperty(property); + FireboltProperties tmpProperties = FireboltProperties.copy(sessionProperties); + propertiesEditor.accept(tmpProperties); validateConnection(tmpProperties, false); - this.sessionProperties.addProperty(property); + propertiesEditor.accept(sessionProperties); } catch (FireboltException e) { throw e; } catch (Exception e) { - throw new FireboltException( - String.format("Could not set property %s=%s", property.getLeft(), property.getRight()), e); + throw new FireboltException(errorMessageFactory.get(), e); } } + public void setEndpoint(String endpoint) { + this.httpConnectionUrl = endpoint; + } + + public String getEndpoint() { + return httpConnectionUrl; + } + @Override @NotImplemented - public void commit() throws SQLException { + public void commit() { // no-op as transactions are not supported } @@ -436,63 +500,46 @@ public void rollback() throws SQLException { // no-op as transactions are not supported } - @Override - public boolean isWrapperFor(Class iface) { - return iface.isAssignableFrom(getClass()); - } - - @Override - public T unwrap(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return iface.cast(this); - } - throw new SQLException("Cannot unwrap to " + iface.getName()); - } - @Override public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { validateConnectionIsNotClose(); - this.networkTimeout = milliseconds; + networkTimeout = milliseconds; } @Override public int getNetworkTimeout() { - return this.networkTimeout; + return networkTimeout; } public int getConnectionTimeout() { - return this.connectionTimeout; + return connectionTimeout; } @Override - @NotImplemented public String nativeSQL(String sql) throws SQLException { - throw new FireboltUnsupportedOperationException(); - } - - @Override - public boolean isReadOnly() throws SQLException { - this.validateConnectionIsNotClose(); - return false; + return translateSQL(sql, true); } - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public void setReadOnly(boolean readOnly) throws SQLException { - // no-op + private String translateSQL(String sql, boolean escapeProcessing) throws SQLException { + if (sql == null) { + throw new IllegalArgumentException("SQL is null"); + } + if (!escapeProcessing || sql.indexOf('{') < 0) { + return sql; + } + throw new SQLWarning("Escape processing is not supported right now", "0A000"); } @Override - @NotImplemented - public SQLWarning getWarnings() throws SQLException { - return null; + public boolean isReadOnly() throws SQLException { + validateConnectionIsNotClose(); + return false; } @Override @ExcludeFromJacocoGeneratedReport @NotImplemented - public void clearWarnings() throws SQLException { + public void setReadOnly(boolean readOnly) { // no-op } @@ -504,7 +551,7 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe @Override @NotImplemented - public Map> getTypeMap() throws SQLException { + public Map> getTypeMap() { // Since setTypeMap is currently not supported, an empty map is returned (refer to the doc for more info) return Map.of(); } @@ -523,7 +570,7 @@ public int getHoldability() throws SQLException { @Override @ExcludeFromJacocoGeneratedReport @NotImplemented - public void setHoldability(int holdability) throws SQLException { + public void setHoldability(int holdability) { // No support for transaction } @@ -562,21 +609,18 @@ public CallableStatement prepareCall(String sql, int resultSetType, int resultSe } @Override - @NotImplemented - public Clob createClob() throws SQLException { - throw new SQLFeatureNotSupportedException(); + public Clob createClob() { + return new FireboltClob(); } @Override - @NotImplemented - public Blob createBlob() throws SQLException { - throw new SQLFeatureNotSupportedException(); + public Blob createBlob() { + return new FireboltBlob(); } @Override - @NotImplemented - public NClob createNClob() throws SQLException { - throw new SQLFeatureNotSupportedException(); + public NClob createNClob() { + return new FireboltClob(); } @Override @@ -586,24 +630,25 @@ public SQLXML createSQLXML() throws SQLException { } @Override + @NotImplemented public void setClientInfo(String name, String value) throws SQLClientInfoException { // Not supported } @Override - @NotImplemented - public String getClientInfo(String name) throws SQLException { - return null; + public String getClientInfo(String name) { + return Optional.ofNullable(FireboltSessionProperty.byAlias(name.toUpperCase()).getValue(sessionProperties)).map(Object::toString).orElse(null); } @Override - @NotImplemented public Properties getClientInfo() throws SQLException { - return new Properties(); + return getNonDeprecatedProperties().stream() + .filter(key -> key.getValue(sessionProperties) != null) + .collect(toMap(FireboltSessionProperty::getKey, key -> key.getValue(sessionProperties).toString(), (o, t) -> t, Properties::new)); } @Override - @ExcludeFromJacocoGeneratedReport + @NotImplemented public void setClientInfo(Properties properties) throws SQLClientInfoException { // Not supported } @@ -619,4 +664,21 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti public Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } + + public String getProtocolVersion() { + return protocolVersion; + } + + public int getInfraVersion() { + return infraVersion; + } + + public void register(CacheListener listener) { + cacheListeners.add(listener); + } + + @Override + public void cleanup() { + cacheListeners.forEach(CacheListener::cleanup); + } } diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecret.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecret.java new file mode 100644 index 00000000..ab08eb34 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecret.java @@ -0,0 +1,153 @@ +package com.firebolt.jdbc.connection; + +import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.client.authentication.AuthenticationRequest; +import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; +import com.firebolt.jdbc.client.authentication.ServiceAccountAuthenticationRequest; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.service.FireboltAccountIdService; +import com.firebolt.jdbc.service.FireboltAuthenticationService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; +import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltEngineVersion2Service; +import com.firebolt.jdbc.service.FireboltGatewayUrlService; +import com.firebolt.jdbc.service.FireboltStatementService; +import com.firebolt.jdbc.util.PropertyUtil; +import lombok.NonNull; +import okhttp3.OkHttpClient; + +import java.net.URL; +import java.sql.SQLException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Properties; + +import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.ACCOUNT_ID; +import static com.firebolt.jdbc.exception.ExceptionType.RESOURCE_NOT_FOUND; +import static java.lang.String.format; + +public class FireboltConnectionServiceSecret extends FireboltConnection { + private static final String PROTOCOL_VERSION = "2.1"; + private final FireboltGatewayUrlService fireboltGatewayUrlService; + private final FireboltAccountIdService fireboltAccountIdService; + private FireboltEngineService fireboltEngineService; // depends on infra version and is discovered during authentication + + FireboltConnectionServiceSecret(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltGatewayUrlService fireboltGatewayUrlService, + FireboltStatementService fireboltStatementService, + FireboltEngineInformationSchemaService fireboltEngineService, + FireboltAccountIdService fireboltAccountIdService) throws SQLException { + super(url, connectionSettings, fireboltAuthenticationService, fireboltStatementService, PROTOCOL_VERSION); + this.fireboltGatewayUrlService = fireboltGatewayUrlService; + this.fireboltAccountIdService = fireboltAccountIdService; + this.fireboltEngineService = fireboltEngineService; + connect(); + } + + @ExcludeFromJacocoGeneratedReport + FireboltConnectionServiceSecret(@NonNull String url, Properties connectionSettings) throws SQLException { + super(url, connectionSettings, PROTOCOL_VERSION); + OkHttpClient httpClient = getHttpClient(loginProperties); + this.fireboltGatewayUrlService = new FireboltGatewayUrlService(createFireboltAccountRetriever(httpClient,"engineUrl", GatewayUrlResponse.class)); + this.fireboltAccountIdService = new FireboltAccountIdService(createFireboltAccountRetriever(httpClient,"resolve", FireboltAccount.class)); + // initialization of fireboltEngineService depends on the infraVersion (the version of engine) + connect(); + } + + private FireboltAccountRetriever createFireboltAccountRetriever(OkHttpClient httpClient, String path, Class type) { + return new FireboltAccountRetriever<>(httpClient, this, loginProperties.getUserDrivers(), loginProperties.getUserClients(), loginProperties.getHost(), path, type); + } + + @Override + protected void authenticate() throws SQLException { + String account = loginProperties.getAccount(); + if (account == null) { + throw new FireboltException("Cannot connect: account is missing"); + } + String accessToken = getAccessToken(loginProperties).orElse(""); + sessionProperties = getSessionPropertiesForSystemEngine(accessToken, account); + assertDatabaseExisting(loginProperties.getDatabase()); + if (!loginProperties.isSystemEngine()) { + sessionProperties = getSessionPropertiesForNonSystemEngine(); + } + } + + private FireboltProperties getSessionPropertiesForNonSystemEngine() throws SQLException { + sessionProperties = sessionProperties.toBuilder().engine(loginProperties.getEngine()).build(); + Engine engine = getFireboltEngineService().getEngine(loginProperties); + // update Firebolt properties. If we are here there are no contradictions between discovered and supplied parameters (db or engine): all validations are done in getEngine() + return loginProperties.toBuilder() + .host(engine.getEndpoint()) // was not know until this point + .engine(engine.getName()) // engine name is updated here because this code is running either if engine has been supplied in initial parameters or when default engine for current DB was discovered + .systemEngine(false) // this is definitely not system engine + .database(engine.getDatabase()) // DB is updated because this code is running either when DB was supplied in initial parameters or not + .accountId(sessionProperties.getAccountId()) // discovered in case of v2 engine + .runtimeAdditionalProperties(sessionProperties.getRuntimeAdditionalProperties()) // discovered in case of v2 engine + .build(); + } + + @Override + protected void assertDatabaseExisting(String database) throws SQLException { + if (database != null && !PropertyUtil.isLocalDb(loginProperties) && !getFireboltEngineService().doesDatabaseExist(database)) { + throw new FireboltException(format("Database %s does not exist", database), RESOURCE_NOT_FOUND); + } + } + + private FireboltProperties getSessionPropertiesForSystemEngine(String accessToken, String accountName) throws SQLException { + String systemEngineEndpoint = fireboltGatewayUrlService.getUrl(accessToken, accountName); + FireboltAccount account = fireboltAccountIdService.getValue(accessToken, accountName); + infraVersion = account.getInfraVersion(); + URL systemEngienUrl = UrlUtil.createUrl(systemEngineEndpoint); + Map systemEngineUrlUrlParams = UrlUtil.getQueryParameters(systemEngienUrl); + String accountId = systemEngineUrlUrlParams.getOrDefault(ACCOUNT_ID.getKey(), account.getId()); + for (Entry e : systemEngineUrlUrlParams.entrySet()) { + loginProperties.addProperty(e); + } + return loginProperties + .toBuilder() + .systemEngine(true) + .compress(false) + .accountId(accountId) + .host(systemEngienUrl.getHost()) + .build(); + } + + + + + private FireboltEngineService getFireboltEngineService() throws SQLException { + if (fireboltEngineService == null) { + int currentInfraVersion = Optional.ofNullable(loginProperties.getAdditionalProperties().get("infraVersion")).map(Integer::parseInt).orElse(infraVersion); + fireboltEngineService = currentInfraVersion >= 2 ? new FireboltEngineVersion2Service(this) : new FireboltEngineInformationSchemaService(this); + } + return fireboltEngineService; + } + + @Override + protected FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { + FireboltProperties properties = super.extractFireboltProperties(jdbcUri, connectionProperties); + if ("".equals(properties.getDatabase())) { + return properties.toBuilder().database(null).build(); + } + return properties; + } + + + @Override + protected FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient) { + return new FireboltAuthenticationClient(httpClient, this, loginProperties.getUserDrivers(), loginProperties.getUserClients()) { + @Override + public AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + return new ServiceAccountAuthenticationRequest(username, password, environment); + } + }; + } + +} diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionTokens.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionTokens.java index 0a014d89..96feba46 100644 --- a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionTokens.java +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionTokens.java @@ -1,12 +1,25 @@ package com.firebolt.jdbc.connection; -import lombok.Builder; -import lombok.Value; +import org.json.JSONObject; -@Value -@Builder public class FireboltConnectionTokens { - String accessToken; - String refreshToken; - long expiresInSeconds; + private final String accessToken; + private final long expiresInSeconds; + + public FireboltConnectionTokens(String accessToken, long expiresInSeconds) { + this.accessToken = accessToken; + this.expiresInSeconds = expiresInSeconds; + } + + FireboltConnectionTokens(JSONObject json) { + this(json.getString("access_token"), json.getLong("expires_in")); + } + + public String getAccessToken() { + return accessToken; + } + + public long getExpiresInSeconds() { + return expiresInSeconds; + } } diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPassword.java b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPassword.java new file mode 100644 index 00000000..e36845c1 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/FireboltConnectionUserPassword.java @@ -0,0 +1,79 @@ +package com.firebolt.jdbc.connection; + +import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.authentication.AuthenticationRequest; +import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; +import com.firebolt.jdbc.client.authentication.UsernamePasswordAuthenticationRequest; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.service.FireboltAuthenticationService; +import com.firebolt.jdbc.service.FireboltEngineApiService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; +import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltStatementService; +import lombok.NonNull; +import okhttp3.OkHttpClient; + +import java.sql.SQLException; +import java.util.Properties; + +public class FireboltConnectionUserPassword extends FireboltConnection { + // Visible for testing + public static final String SYSTEM_ENGINE_NAME = "system"; + private static final String PROTOCOL_VERSION = null; // It could be 1.0, but we send null for backwards compatibility, so not version header is sent + private final FireboltEngineService fireboltEngineService; + + FireboltConnectionUserPassword(@NonNull String url, + Properties connectionSettings, + FireboltAuthenticationService fireboltAuthenticationService, + FireboltStatementService fireboltStatementService, + FireboltEngineInformationSchemaService fireboltEngineService) throws SQLException { + super(url, connectionSettings, fireboltAuthenticationService, fireboltStatementService, PROTOCOL_VERSION); + this.fireboltEngineService = fireboltEngineService; + connect(); + } + + @ExcludeFromJacocoGeneratedReport + FireboltConnectionUserPassword(@NonNull String url, Properties connectionSettings) throws SQLException { + super(url, connectionSettings, PROTOCOL_VERSION); + OkHttpClient httpClient = getHttpClient(loginProperties); + this.fireboltEngineService = new FireboltEngineApiService(new FireboltAccountClient(httpClient, this, loginProperties.getUserDrivers(), loginProperties.getUserClients())); + connect(); + } + + @Override + protected void authenticate() throws SQLException { + String accessToken = getAccessToken(loginProperties).orElse(""); + FireboltProperties propertiesWithAccessToken = loginProperties.toBuilder().accessToken(accessToken).build(); + Engine engine = fireboltEngineService.getEngine(propertiesWithAccessToken); + String database = loginProperties.getDatabase(); + if (engine.getDatabase() != null) { + database = engine.getDatabase(); + } + sessionProperties = loginProperties.toBuilder().host(engine.getEndpoint()).engine(engine.getName()).database(database).build(); + } + + @Override + protected FireboltProperties extractFireboltProperties(String jdbcUri, Properties connectionProperties) { + FireboltProperties properties = super.extractFireboltProperties(jdbcUri, connectionProperties); + boolean systemEngine = SYSTEM_ENGINE_NAME.equals(properties.getEngine()); + boolean compressed = !systemEngine && properties.isCompress(); + return properties.toBuilder().systemEngine(systemEngine).compress(compressed).build(); + } + + @Override + protected void assertDatabaseExisting(String database) { + // empty implementation. There is no way to validate that DB exists. Even if such API exists it is irrelevant + // because it is used for old DB that will be obsolete soon and only when using either system or local engine. + } + + @Override + protected FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient) { + return new FireboltAuthenticationClient(httpClient,this, loginProperties.getUserDrivers(), loginProperties.getUserClients()) { + @Override + public AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + return new UsernamePasswordAuthenticationRequest(username, password, host); + } + }; + } +} diff --git a/src/main/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtil.java b/src/main/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtil.java deleted file mode 100644 index b509c8d6..00000000 --- a/src/main/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtil.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.firebolt.jdbc.connection; - -import java.net.URI; -import java.util.Optional; -import java.util.Properties; - -import org.apache.commons.lang3.StringUtils; - -import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; - -import lombok.CustomLog; -import lombok.experimental.UtilityClass; - -@CustomLog -@UtilityClass -public class FireboltJdbcUrlUtil { - - public static final String JDBC_PREFIX = "jdbc:"; - - public static Properties extractProperties(String jdbcUrl) { - URI uri = extractUriFromJdbcUrl(jdbcUrl); - return parseUriQueryPart(uri); - } - - private URI extractUriFromJdbcUrl(String jdbcConnectionString) { - String cleanURI = StringUtils.replace(jdbcConnectionString, JDBC_PREFIX, ""); - return URI.create(cleanURI); - } - - private static Properties parseUriQueryPart(URI uri) { - Properties uriProperties = new Properties(); - String query = uri.getQuery(); - if (StringUtils.isNotBlank(query)) { - String[] queryKeyValues = query.split("&"); - for (String keyValue : queryKeyValues) { - String[] keyValueTokens = keyValue.split("="); - if (keyValueTokens.length == 2) { - uriProperties.put(keyValueTokens[0], keyValueTokens[1]); - } else { - log.warn("Cannot parse key-pair: {}", keyValue); - } - } - } - Optional.ofNullable(uri.getPath()) - .ifPresent(path -> uriProperties.put(FireboltSessionProperty.PATH.getKey(), path)); - Optional.ofNullable(uri.getHost()) - .ifPresent(host -> uriProperties.put(FireboltSessionProperty.HOST.getKey(), host)); - Optional.of(uri.getPort()).filter(p -> !p.equals(-1)) - .ifPresent(port -> uriProperties.put(FireboltSessionProperty.PORT.getKey(), String.valueOf(port))); - return uriProperties; - } -} diff --git a/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java b/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java new file mode 100644 index 00000000..1142403e --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/connection/UrlUtil.java @@ -0,0 +1,83 @@ +package com.firebolt.jdbc.connection; + +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; +import lombok.experimental.UtilityClass; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.util.stream.Collectors.toMap; + +@UtilityClass +public class UrlUtil { + + public static final String JDBC_PREFIX = "jdbc:firebolt:"; + private static final Logger log = Logger.getLogger(UrlUtil.class.getName()); + + public static Properties extractProperties(String jdbcUrl) { + return parseUriQueryPart(jdbcUrl); + } + + + private static Properties parseUriQueryPart(String jdbcConnectionString) { + String cleanURI = jdbcConnectionString.replace(JDBC_PREFIX, ""); + URI uri = URI.create(cleanURI); + Properties uriProperties = new Properties(); + String query = uri.getQuery(); + if (query != null && !query.isBlank()) { + String[] queryKeyValues = query.split("&"); + for (String keyValue : queryKeyValues) { + String[] keyValueTokens = keyValue.split("="); + if (keyValueTokens.length == 2) { + uriProperties.put(keyValueTokens[0], keyValueTokens[1]); + } else { + log.log(Level.WARNING, "Cannot parse key-pair: {0}", keyValue); + } + } + } + Optional.ofNullable(uri.getPath()).map(p -> !p.isEmpty() && p.charAt(p.length() - 1) == '/' ? p.substring(0, p.length() - 1) : p) + .ifPresent(path -> uriProperties.put(FireboltSessionProperty.PATH.getKey(), path)); + Optional.ofNullable(uri.getHost()) + .ifPresent(host -> uriProperties.put(FireboltSessionProperty.HOST.getKey(), host)); + Optional.of(uri.getPort()).filter(p -> !p.equals(-1)) + .ifPresent(port -> uriProperties.put(FireboltSessionProperty.PORT.getKey(), String.valueOf(port))); + return uriProperties; + } + + /** + * This factory method is similar to {@link URI#create(String)}. + * The difference is that `URI.host` of {@code http://something} is {@code null} while + * URL spec {@code http://something/} returns URI with host=something. + * @param spec – the String to parse as a URL. + * @return URL instance + */ + public static URL createUrl(String spec) { + try { + return new URL(spec); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + @SuppressWarnings("java:S3358") // ternary operator for null-safe extraction of value + public static Map getQueryParameters(URL url) { + String query = url.getQuery(); + if (query == null || query.isBlank()) { + return Map.of(); + } + return Arrays.stream(query.split("&")).map(String::trim).filter(kv -> !kv.isBlank()).map(kv -> kv.split("=", 2)).filter(kv -> kv.length == 2).collect(toMap( + kv -> kv[0], + kv -> kv[1], + (first, second) -> second, // override duplicate value + () -> new TreeMap<>(CASE_INSENSITIVE_ORDER))); // the URL parameters are case-insensitive + } +} diff --git a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java index 6fe3118e..e3ddc9de 100644 --- a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java +++ b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltProperties.java @@ -1,27 +1,41 @@ package com.firebolt.jdbc.connection.settings; -import java.util.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.ToString; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; - -import lombok.Builder; -import lombok.CustomLog; -import lombok.NonNull; -import lombok.Value; +import static com.firebolt.jdbc.connection.FireboltConnectionUserPassword.SYSTEM_ENGINE_NAME; +import static com.firebolt.jdbc.util.PropertyUtil.mergeProperties; +import static java.lang.String.format; -@Value +@Getter +@ToString +@AllArgsConstructor +@EqualsAndHashCode @Builder(toBuilder = true) -@CustomLog public class FireboltProperties { - private static final Pattern DB_PATH_PATTERN = Pattern.compile("/([a-zA-Z0-9_*\\-]+)"); + private static final Pattern DB_PATH_PATTERN = Pattern.compile("/?([a-zA-Z0-9_*\\-]+)"); private static final int FIREBOLT_SSL_PROXY_PORT = 443; private static final int FIREBOLT_NO_SSL_PROXY_PORT = 9090; - private static final String SYSTEM_ENGINE_NAME = "system"; private static final Set sessionPropertyKeys = Arrays.stream(FireboltSessionProperty.values()) .map(property -> { @@ -31,89 +45,118 @@ public class FireboltProperties { return keys; }).flatMap(List::stream).collect(Collectors.toSet()); - int keepAliveTimeoutMillis; - int maxConnectionsTotal; - int maxRetries; - int bufferSize; - int clientBufferSize; - int socketTimeoutMillis; - int connectionTimeoutMillis; - Integer port; - String host; - String database; - String path; - boolean ssl; - String sslCertificatePath; - String sslMode; - boolean compress; - String user; - String password; - String engine; - String account; - Integer tcpKeepIdle; - Integer tcpKeepCount; - Integer tcpKeepInterval; - boolean logResultSet; - boolean systemEngine; - String userDrivers; - String userClients; - String accessToken; - + private final int keepAliveTimeoutMillis; + private final int maxConnectionsTotal; + private final int maxRetries; + private final int bufferSize; + private final int socketTimeoutMillis; + private final int connectionTimeoutMillis; + private final Integer port; + private final String host; + private String database; // updatable using use statement + private final String path; + private final boolean ssl; + private final String sslCertificatePath; + private final String sslMode; + private final boolean compress; + private final String principal; + private final String secret; + private String engine; // updatable using use statement + private final String account; + private String accountId; + private final int tcpKeepIdle; + private final int tcpKeepCount; + private final int tcpKeepInterval; + private final boolean logResultSet; + private boolean systemEngine; + private final String environment; + private final String userDrivers; + private final String userClients; + private final String accessToken; + @Builder.Default + private Map initialAdditionalProperties = new HashMap<>(); @Builder.Default - Map additionalProperties = new HashMap<>(); - - public static FireboltProperties of(Properties... properties) { - Properties mergedProperties = mergeProperties(properties); - boolean ssl = getSetting(mergedProperties, FireboltSessionProperty.SSL); - String sslRootCertificate = getSetting(mergedProperties, FireboltSessionProperty.SSL_CERTIFICATE_PATH); - String sslMode = getSetting(mergedProperties, FireboltSessionProperty.SSL_MODE); - String user = getSetting(mergedProperties, FireboltSessionProperty.USER); - String password = getSetting(mergedProperties, FireboltSessionProperty.PASSWORD); - String path = getSetting(mergedProperties, FireboltSessionProperty.PATH); - String engine = getSetting(mergedProperties, FireboltSessionProperty.ENGINE); - boolean isSystemEngine = isSystemEngine(engine); - boolean compress = ((Boolean) getSetting(mergedProperties, FireboltSessionProperty.COMPRESS)) - && !isSystemEngine; - String account = getSetting(mergedProperties, FireboltSessionProperty.ACCOUNT); - int keepAliveMillis = getSetting(mergedProperties, FireboltSessionProperty.KEEP_ALIVE_TIMEOUT_MILLIS); - int maxTotal = getSetting(mergedProperties, FireboltSessionProperty.MAX_CONNECTIONS_TOTAL); - int maxRetries = getSetting(mergedProperties, FireboltSessionProperty.MAX_RETRIES); - int bufferSize = getSetting(mergedProperties, FireboltSessionProperty.BUFFER_SIZE); - int socketTimeout = getSetting(mergedProperties, FireboltSessionProperty.SOCKET_TIMEOUT_MILLIS); - int connectionTimeout = getSetting(mergedProperties, FireboltSessionProperty.CONNECTION_TIMEOUT_MILLIS); - int tcpKeepInterval = getSetting(mergedProperties, FireboltSessionProperty.TCP_KEEP_INTERVAL); - int tcpKeepIdle = getSetting(mergedProperties, FireboltSessionProperty.TCP_KEEP_IDLE); - int tcpKeepCount = getSetting(mergedProperties, FireboltSessionProperty.TCP_KEEP_COUNT); - boolean logResultSet = getSetting(mergedProperties, FireboltSessionProperty.LOG_RESULT_SET); - String driverVersions = getSetting(mergedProperties, FireboltSessionProperty.USER_DRIVERS); - String clientVersions = getSetting(mergedProperties, FireboltSessionProperty.USER_CLIENTS); - - String host = getHost(mergedProperties); - Integer port = getPort(mergedProperties, ssl); - String database = getDatabase(mergedProperties, path); - String accessToken = getSetting(mergedProperties, FireboltSessionProperty.ACCESS_TOKEN); - Map additionalProperties = getFireboltCustomProperties(mergedProperties); - - return FireboltProperties.builder().ssl(ssl).sslCertificatePath(sslRootCertificate).sslMode(sslMode).path(path) - .port(port).database(database).compress(compress).user(user).password(password).host(host) - .additionalProperties(additionalProperties).account(account).engine(engine) - .keepAliveTimeoutMillis(keepAliveMillis).maxConnectionsTotal(maxTotal).maxRetries(maxRetries) - .bufferSize(bufferSize).socketTimeoutMillis(socketTimeout).connectionTimeoutMillis(connectionTimeout) - .tcpKeepInterval(tcpKeepInterval).tcpKeepCount(tcpKeepCount).tcpKeepIdle(tcpKeepIdle) - .logResultSet(logResultSet).systemEngine(isSystemEngine) - .userDrivers(driverVersions) - .userClients(clientVersions) - .accessToken(accessToken) - .build(); - } - - private static String getHost(Properties properties) { + private Map runtimeAdditionalProperties = new HashMap<>(); + + public FireboltProperties(Properties[] allProperties) { + this(mergeProperties(allProperties)); + } + + public FireboltProperties(Properties properties) { + ssl = getSetting(properties, FireboltSessionProperty.SSL); + sslCertificatePath = getSetting(properties, FireboltSessionProperty.SSL_CERTIFICATE_PATH); + sslMode = getSetting(properties, FireboltSessionProperty.SSL_MODE); + principal = getSetting(properties, FireboltSessionProperty.CLIENT_ID); + secret = getSetting(properties, FireboltSessionProperty.CLIENT_SECRET); + path = getSetting(properties, FireboltSessionProperty.PATH); + database = getDatabase(properties, path); + engine = getEngine(properties); + systemEngine = isSystemEngine(engine); + compress = ((Boolean) getSetting(properties, FireboltSessionProperty.COMPRESS)) && !systemEngine; + account = getSetting(properties, FireboltSessionProperty.ACCOUNT); + accountId = getSetting(properties, FireboltSessionProperty.ACCOUNT_ID); + keepAliveTimeoutMillis = getSetting(properties, FireboltSessionProperty.KEEP_ALIVE_TIMEOUT_MILLIS); + maxConnectionsTotal = getSetting(properties, FireboltSessionProperty.MAX_CONNECTIONS_TOTAL); + maxRetries = getSetting(properties, FireboltSessionProperty.MAX_RETRIES); + bufferSize = getSetting(properties, FireboltSessionProperty.BUFFER_SIZE); + socketTimeoutMillis = getSetting(properties, FireboltSessionProperty.SOCKET_TIMEOUT_MILLIS); + connectionTimeoutMillis = getSetting(properties, FireboltSessionProperty.CONNECTION_TIMEOUT_MILLIS); + tcpKeepInterval = getSetting(properties, FireboltSessionProperty.TCP_KEEP_INTERVAL); + tcpKeepIdle = getSetting(properties, FireboltSessionProperty.TCP_KEEP_IDLE); + tcpKeepCount = getSetting(properties, FireboltSessionProperty.TCP_KEEP_COUNT); + logResultSet = getSetting(properties, FireboltSessionProperty.LOG_RESULT_SET); + String configuredEnvironment = getSetting(properties, FireboltSessionProperty.ENVIRONMENT); + userDrivers = getSetting(properties, FireboltSessionProperty.USER_DRIVERS); + userClients = getSetting(properties, FireboltSessionProperty.USER_CLIENTS); + + environment = getEnvironment(configuredEnvironment, properties); + host = getHost(configuredEnvironment, properties); + port = getPort(properties, ssl); + accessToken = getSetting(properties, FireboltSessionProperty.ACCESS_TOKEN); + + initialAdditionalProperties = getFireboltCustomProperties(properties); + runtimeAdditionalProperties = new HashMap<>(); + } + + private static String getEngine(Properties mergedProperties) { + return getSetting(mergedProperties, FireboltSessionProperty.ENGINE); + } + + private static String getHost(String environment, Properties properties ) { String host = getSetting(properties, FireboltSessionProperty.HOST); - if (StringUtils.isEmpty(host)) { - throw new IllegalArgumentException("Invalid host: The host is missing or empty"); - } else { - return host; + return host == null || host.isEmpty() ? format("api.%s.firebolt.io", environment) : host; + } + + /** + * Discovers environment name from host if it matches pattern {@code api.ENV.firebolt.io} + * @param environment - the environment from properties or default value as defined in {@link FireboltSessionProperty#ENVIRONMENT} + * @param properties - configuration properties + * @return the environment value + * @throws IllegalStateException if environment extracted from host is not equal to given one. + */ + private static String getEnvironment(String environment, @NotNull Properties properties) { + Pattern environmentalHost = Pattern.compile("api\\.(.+?)\\.firebolt\\.io"); + String envFromProps = Stream.concat(Stream.of(FireboltSessionProperty.ENVIRONMENT.getKey()), Stream.of(FireboltSessionProperty.ENVIRONMENT.getAliases())) + .map(properties::getProperty) + .filter(Objects::nonNull).findFirst() + .orElse(null); + String envFromHost = null; + String host = getSetting(properties, FireboltSessionProperty.HOST); + if (host != null) { + Matcher m = environmentalHost.matcher(host); + if (m.find() && m.group(1) != null) { + envFromHost = m.group(1); + } + } + if (envFromHost != null) { + if (envFromProps == null) { + return envFromHost; + } + if (!Objects.equals(environment, envFromHost)) { + throw new IllegalStateException(format("Environment %s does not match host %s", environment, host)); + } } + return environment; } @NonNull @@ -127,15 +170,15 @@ private static Integer getPort(Properties properties, boolean ssl) { private static String getDatabase(Properties properties, String path) throws IllegalArgumentException { String database = getSetting(properties, FireboltSessionProperty.DATABASE); - if (StringUtils.isEmpty(database)) { - if ("/".equals(path)) { - throw new IllegalArgumentException("A database must be provided"); + if (database == null || database.isEmpty()) { + if ("/".equals(path) || "".equals(path)) { + return null; } else { Matcher m = DB_PATH_PATTERN.matcher(path); if (m.matches()) { return m.group(1); } else { - throw new IllegalArgumentException(String.format("The database provided is invalid %s", path)); + throw new IllegalArgumentException(format("The database provided is invalid %s", path)); } } } else { @@ -170,38 +213,64 @@ private static T getSetting(Properties info, FireboltSessionProperty param) return (T) clazz.cast(Long.valueOf(val)); } if (clazz == boolean.class || clazz == Boolean.class) { - boolean boolValue; - if (StringUtils.isNumeric(val)) { - boolValue = Integer.parseInt(val) > 0; - } else { - boolValue = Boolean.parseBoolean(val); - } + boolean boolValue = val.chars().allMatch(Character::isDigit) ? Integer.parseInt(val) > 0 : Boolean.parseBoolean(val); return (T) clazz.cast(boolValue); } return (T) clazz.cast(val); } - private static Properties mergeProperties(Properties... properties) { - Properties mergedProperties = new Properties(); - for (Properties p : properties) { - mergedProperties.putAll(p); - } - return mergedProperties; - } - public static FireboltProperties copy(FireboltProperties properties) { - return properties.toBuilder().additionalProperties(new HashMap<>(properties.getAdditionalProperties())).build(); + return properties.toBuilder().runtimeAdditionalProperties(new HashMap<>(properties.getRuntimeAdditionalProperties())).build(); } private static boolean isSystemEngine(String engine) { - return StringUtils.equalsIgnoreCase(SYSTEM_ENGINE_NAME, engine); + return engine == null; + } + + public Map getAdditionalProperties() { + Map additionalProperties = new HashMap<>(initialAdditionalProperties); + additionalProperties.putAll(runtimeAdditionalProperties); + return additionalProperties; } public void addProperty(@NonNull String key, String value) { - additionalProperties.put(key, value); + // This a bad patch but there is nothing to do right now. We will refactor this class and make solution more generic + switch (key) { + case "database": database = value; break; + case "engine": + engine = value; + systemEngine = SYSTEM_ENGINE_NAME.equalsIgnoreCase(engine); + break; + case "account_id": + if (accountId != null && !accountId.equalsIgnoreCase(value)) { + throw new IllegalStateException("Failed to execute command. Account parameter mismatch. Contact support"); + } + this.accountId = value; + break; + default: runtimeAdditionalProperties.put(key, value); + } + } + + public void addProperty(Entry property) { + addProperty(property.getKey(), property.getValue()); + } + + public String getHttpConnectionUrl() { + String hostAndPort = host + (port == null ? "" : ":" + port); + String protocol = isSsl() ? "https://" : "http://"; + return protocol + hostAndPort; + } + + public void clearAdditionalProperties() { + runtimeAdditionalProperties.clear(); } - public void addProperty(Pair property) { - this.addProperty(property.getLeft(), property.getRight()); + public String processEngineUrl(String endpoint) { + String[] engineUrl = endpoint.split("\\?", 2); + String engineHost = engineUrl[0].replaceFirst("^https?://", ""); // just in case remove URL scheme although right now server never returns it + String[] engineQuery = engineUrl.length > 1 ? engineUrl[1].split("&") : new String[0]; + // get properties from query string and update values + Arrays.stream(engineQuery).map(prop -> prop.split("=")).filter(a -> a.length == 2).forEach(prop -> addProperty(prop[0], prop[1])); + return engineHost; } } diff --git a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltQueryParameterKey.java b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltQueryParameterKey.java index ee2a183a..1463c0e8 100644 --- a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltQueryParameterKey.java +++ b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltQueryParameterKey.java @@ -6,8 +6,15 @@ @RequiredArgsConstructor @Getter public enum FireboltQueryParameterKey { - DATABASE("database"), QUERY_ID("query_id"), COMPRESS("compress"), DEFAULT_FORMAT("default_format"), - OUTPUT_FORMAT("output_format"); + DATABASE("database"), + ENGINE("engine"), + QUERY_ID("query_id"), + QUERY_LABEL("query_label"), + COMPRESS("compress"), + DEFAULT_FORMAT("default_format"), + OUTPUT_FORMAT("output_format"), + ACCOUNT_ID("account_id"), + ; private final String key; } \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltSessionProperty.java b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltSessionProperty.java index 71621e3c..89502466 100644 --- a/src/main/java/com/firebolt/jdbc/connection/settings/FireboltSessionProperty.java +++ b/src/main/java/com/firebolt/jdbc/connection/settings/FireboltSessionProperty.java @@ -1,103 +1,118 @@ package com.firebolt.jdbc.connection.settings; +import lombok.Getter; + import java.lang.reflect.Field; import java.util.Arrays; import java.util.List; -import java.util.Optional; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.function.Function; +import java.util.stream.Collector; import java.util.stream.Collectors; +import java.util.stream.Stream; -import lombok.Getter; +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.util.stream.Collectors.toMap; @Getter public enum FireboltSessionProperty { - PATH("path", "/", String.class, "Path component of the URI"), - BUFFER_SIZE("buffer_size", 65536, Integer.class, "The buffer used to create the ResultSet in bytes"), - SSL("ssl", true, Boolean.class, "Enable SSL/TLS for the connection"), - SSL_CERTIFICATE_PATH("ssl_certificate_path", "", String.class, "SSL/TLS root certificate", "sslrootcert"), + PATH("path", "", String.class, "Path component of the URI", FireboltProperties::getPath), + BUFFER_SIZE("buffer_size", 65536, Integer.class, "The buffer used to create the ResultSet in bytes", FireboltProperties::getBufferSize), + SSL("ssl", true, Boolean.class, "Enable SSL/TLS for the connection", FireboltProperties::isSsl), + SSL_CERTIFICATE_PATH("ssl_certificate_path", "", String.class, "SSL/TLS root certificate", FireboltProperties::getSslCertificatePath, "sslrootcert"), SSL_MODE("ssl_mode", "strict", String.class, - "SSL mode to verify/not verify the certificate. Supported Types: none (don't verify), strict (verify)", + "SSL mode to verify/not verify the certificate. Supported Types: none (don't verify), strict (verify)", FireboltProperties::getSslMode, "sslmode"), MAX_RETRIES("max_retries", 3, Integer.class, - "Maximum number of retries used by the client to query Firebolt when the response has an invalid status code that is retryable (HTTP_CLIENT_TIMEOUT/408, HTTP_BAD_GATEWAY/502, HTTP_UNAVAILABLE/503 or HTTP_GATEWAY_TIMEOUT/504). Set to 0 to disable "), + "Maximum number of retries used by the client to query Firebolt when the response has an invalid status code that is retryable (HTTP_CLIENT_TIMEOUT/408, HTTP_BAD_GATEWAY/502, HTTP_UNAVAILABLE/503 or HTTP_GATEWAY_TIMEOUT/504). Set to 0 to disable ", FireboltProperties::getMaxRetries), SOCKET_TIMEOUT_MILLIS("socket_timeout_millis", 0, Integer.class, - "maximum time of inactivity between two data packets when exchanging data with the server. A timeout value of zero is interpreted as an infinite timeout. A negative value is interpreted as undefined.", + "maximum time of inactivity between two data packets when exchanging data with the server. A timeout value of zero is interpreted as an infinite timeout. A negative value is interpreted as undefined.", FireboltProperties::getSocketTimeoutMillis, "socket_timeout"), CONNECTION_TIMEOUT_MILLIS("connection_timeout_millis", 60 * 1000, Integer.class, - "Default connect timeout for new connections. A value of 0 means no timeout, otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds", + "Default connect timeout for new connections. A value of 0 means no timeout, otherwise values must be between 1 and Integer.MAX_VALUE when converted to milliseconds", FireboltProperties::getConnectionTimeoutMillis, "connection_timeout"), KEEP_ALIVE_TIMEOUT_MILLIS("connection_keep_alive_timeout_millis", 5 * 60 * 1000, Integer.class, - "How long to keep a connection with the server alive in the pool before closing it.", "keepAliveTimeout"), + "How long to keep a connection with the server alive in the pool before closing it.", FireboltProperties::getKeepAliveTimeoutMillis, "keepAliveTimeout"), MAX_CONNECTIONS_TOTAL("max_connections_total", 300, Integer.class, - "Maximum total connections in the connection pool", "maxTotal"), + "Maximum total connections in the connection pool", FireboltProperties::getMaxConnectionsTotal, "maxTotal"), TCP_KEEP_IDLE("tcp_keep_idle", 60, Integer.class, - "TCP option that defines the number of seconds of idle time before keep-alive initiates a probe. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe after some amount of time."), + "TCP option that defines the number of seconds of idle time before keep-alive initiates a probe. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe after some amount of time.", FireboltProperties::getTcpKeepIdle), TCP_KEEP_COUNT("tcp_keep_count", 10, Integer.class, - "TCP option that defines the maximum number of keep-alive probes to be sent. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe a certain number of times before a connection is considered to be broken."), + "TCP option that defines the maximum number of keep-alive probes to be sent. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe a certain number of times before a connection is considered to be broken.", FireboltProperties::getTcpKeepCount), TCP_KEEP_INTERVAL("tcp_keep_interval", 30, Integer.class, - "TCP option that defines the number of seconds to wait before retransmitting a keep-alive probe. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe after some amount of time."), + "TCP option that defines the number of seconds to wait before retransmitting a keep-alive probe. TCP probes a connection that has been idle for some amount of time. If the remote system does not respond to a keep-alive probe, TCP retransmits the probe after some amount of time.", FireboltProperties::getTcpKeepInterval), COMPRESS( /* * compress should always be used as the HTTP response code is sometimes * incorrect when not using it */ - "compress", true, Boolean.class, "Whether to compress transferred data or not. Compressed by default"), - DATABASE("database", null, String.class, "default database name"), - PASSWORD("password", null, String.class, "user password - null by default"), - USER("user", null, String.class, "user name - null by default"), - HOST("host", null, String.class, "Firebolt host - null by default"), - PORT("port", null, Integer.class, "Firebolt port - null by default"), - ENGINE("engine", null, String.class, "engine - null by default", "engine_name"), - ACCOUNT("account", null, String.class, "account - null by default"), + "compress", true, Boolean.class, "Whether to compress transferred data or not. Compressed by default", FireboltProperties::isCompress), + DATABASE("database", null, String.class, "default database name", FireboltProperties::getDatabase), + // Typically client_secret property should be used, but password is the standard JDBC property supported by all tools, so it is silently defined here as alias. Also see CLIENT_ID. + CLIENT_SECRET("client_secret", null, String.class, "client secret - null by default", p -> "****", "password"), + // Typically client_id property should be used, but user is the standard JDBC property supported by all tools, so it is silently defined here as alias. Also see CLIENT_SECRET + CLIENT_ID("client_id", null, String.class, "client ID - null by default", FireboltProperties::getPrincipal, "user"), + HOST("host", null, String.class, "Firebolt host - null by default", FireboltProperties::getHost), + PORT("port", null, Integer.class, "Firebolt port - null by default", FireboltProperties::getPort), + ENGINE("engine", null, String.class, "engine - null by default", FireboltProperties::getEngine, "engine_name"), + ACCOUNT("account", null, String.class, "account - null by default", FireboltProperties::getAccount), + ACCOUNT_ID("account_id", null, String.class, "accountId - null by default", FireboltProperties::getAccountId), LOG_RESULT_SET("log_result_set", false, Boolean.class, - "When set to true, the result of the queries executed are logged with the log level INFO. This has a negative performance impact and should be enabled only for debugging purposes"), - USER_DRIVERS("user_drivers", null, String.class, "user drivers"), - USER_CLIENTS("user_clients", null, String.class, "user clients"), - ACCESS_TOKEN("access_token", null, String.class, "access token"), - + "When set to true, the result of the queries executed are logged with the log level INFO. This has a negative performance impact and should be enabled only for debugging purposes", FireboltProperties::isLogResultSet), + USER_DRIVERS("user_drivers", null, String.class, "user drivers", FireboltProperties::getUserDrivers), + USER_CLIENTS("user_clients", null, String.class, "user clients", FireboltProperties::getUserClients), + ACCESS_TOKEN("access_token", null, String.class, "access token", p -> "***"), + ENVIRONMENT("environment", "app", String.class, "Firebolt environment", FireboltProperties::getEnvironment, "env"), // We keep all the deprecated properties to ensure backward compatibility - but // they do not have any effect. @Deprecated TIME_TO_LIVE_MILLIS("time_to_live_millis", 60 * 1000, Integer.class, - "Maximum life span of connections regardless of their connection_keep_alive_timeout_millis", + "Maximum life span of connections regardless of their connection_keep_alive_timeout_millis", p -> null, "timeToLiveMillis"), @Deprecated - MAX_CONNECTIONS_PER_ROUTE("max_connections_per_route", 500, Integer.class, "Maximum total connections per route", + MAX_CONNECTIONS_PER_ROUTE("max_connections_per_route", 500, Integer.class, "Maximum total connections per route", p -> null, "defaultMaxPerRoute"), @Deprecated USE_PATH_AS_DB("use_path_as_db", null, Boolean.class, - "When set to true (the default) or not specified, the path parameter from the URL is used as the database name"), + "When set to true (the default) or not specified, the path parameter from the URL is used as the database name", p -> null), @Deprecated USE_CONNECTION_POOL("use_connection_pool", true, Boolean.class, - "use connection pool for valid connections. This property is deprecated and setting it has no effect."), + "use connection pool for valid connections. This property is deprecated and setting it has no effect.", p -> null), @Deprecated VALIDATE_AFTER_INACTIVITY_MILLIS("validate_after_inactivity_millis", 3 * 1000, Integer.class, - "Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer. Non-positive value disables connection validation. "), + "Defines period of inactivity in milliseconds after which persistent connections must be re-validated prior to being leased to the consumer. Non-positive value disables connection validation. ", p -> null), @Deprecated CLIENT_BUFFER_SIZE("client_buffer_size", 65536, Integer.class, "The buffer for the Apache client used by the Driver (in bytes). It is the preferred buffer size for the body of the http response. A larger buffer allows more content to be written before anything is actually sent while a smaller buffer decreases server memory load and allows the client to start receiving data quicker.\n" - + "The buffer will be at least as large as the size requested.", + + "The buffer will be at least as large as the size requested.", p -> null, "apache_buffer_size"), @Deprecated AGGRESSIVE_CANCEL("aggressive_cancel", false, Boolean.class, - "enable aggressive cancel. Permits to cancel queries by sending a query to Firebolt rather than calling the /cancel endpoint"); + "enable aggressive cancel. Permits to cancel queries by sending a query to Firebolt rather than calling the /cancel endpoint", p -> null); private final String key; private final Object defaultValue; private final Class clazz; private final String description; + private final Function valueGetter; private final String[] aliases; + private static final Map aliasToProperty = + Arrays.stream(values()).flatMap(FireboltSessionProperty::getAllPropertyMapping).collect(caseInsensitiveMap()); - FireboltSessionProperty(String key, Object defaultValue, Class clazz, String description, String... aliases) { + FireboltSessionProperty(String key, Object defaultValue, Class clazz, String description, Function valueGetter, String... aliases) { this.key = key; this.defaultValue = defaultValue; this.clazz = clazz; this.description = description; + this.valueGetter = valueGetter; this.aliases = aliases != null ? aliases : new String[] {}; } @@ -112,11 +127,27 @@ public static List getNonDeprecatedProperties() { }).collect(Collectors.toList()); } - public static Optional of(String key) { - return Arrays.stream(values()).filter(v -> v.key.equals(key)).findAny(); + public static FireboltSessionProperty byAlias(String keyOrAlias) { + return aliasToProperty.get(keyOrAlias); } public String[] getPossibleValues() { return Boolean.class.equals(clazz) || Boolean.TYPE.equals(clazz) ? new String[] { "true", "false" } : null; } + + public Object getValue(FireboltProperties fireboltProperties) { + return valueGetter.apply(fireboltProperties); + } + + private static Stream getAllAliases(FireboltSessionProperty property) { + return Stream.concat(Stream.of(property.key), Arrays.stream(property.aliases)); + } + + private static Stream> getAllPropertyMapping(FireboltSessionProperty property) { + return getAllAliases(property).map(a -> Map.entry(a, property)); + } + + private static Collector, ?, Map> caseInsensitiveMap() { + return toMap(Entry::getKey, Entry::getValue, (o, t) -> t, () -> new TreeMap<>(CASE_INSENSITIVE_ORDER)); + } } diff --git a/src/main/java/com/firebolt/jdbc/exception/FireboltException.java b/src/main/java/com/firebolt/jdbc/exception/FireboltException.java index 4c0ead1d..ede3445a 100644 --- a/src/main/java/com/firebolt/jdbc/exception/FireboltException.java +++ b/src/main/java/com/firebolt/jdbc/exception/FireboltException.java @@ -39,10 +39,20 @@ public FireboltException(String message, Integer httpStatusCode, String errorMes this.errorMessageFromServer = errorMessageFromServer; } + public FireboltException(String message, Integer httpStatusCode, String errorMessageFromServer, SQLState state) { + super(message, state.getCode()); + type = getExceptionType(httpStatusCode); + this.errorMessageFromServer = errorMessageFromServer; + } + public FireboltException(String message, Throwable cause) { this(message, cause, ExceptionType.ERROR); } + public FireboltException(String message, Throwable cause, SQLState state) { + this(message, cause, ExceptionType.ERROR, state); + } + public FireboltException(String message, ExceptionType type) { super(message); this.type = type; @@ -59,6 +69,18 @@ public FireboltException(String message, Throwable cause, ExceptionType type) { errorMessageFromServer = null; } + public FireboltException(String message, Throwable cause, ExceptionType type, SQLState state) { + super(message, state.getCode(), cause); + this.type = type; + errorMessageFromServer = null; + } + + public FireboltException(String message, int httpStatusCode, SQLState state) { + super(message, state.getCode()); + type = getExceptionType(httpStatusCode); + errorMessageFromServer = null; + } + private static ExceptionType getExceptionType(Integer httpStatusCode) { if (httpStatusCode == null) { return ERROR; diff --git a/src/main/java/com/firebolt/jdbc/exception/FireboltSQLFeatureNotSupportedException.java b/src/main/java/com/firebolt/jdbc/exception/FireboltSQLFeatureNotSupportedException.java index 99468192..66b61f0b 100644 --- a/src/main/java/com/firebolt/jdbc/exception/FireboltSQLFeatureNotSupportedException.java +++ b/src/main/java/com/firebolt/jdbc/exception/FireboltSQLFeatureNotSupportedException.java @@ -10,4 +10,8 @@ public FireboltSQLFeatureNotSupportedException() { super(String.format(FEATURE_NOT_SUPPORTED, Thread.currentThread().getStackTrace()[2].getMethodName(), Thread.currentThread().getStackTrace()[2].getLineNumber())); } + + public FireboltSQLFeatureNotSupportedException(String message) { + super(message); + } } diff --git a/src/main/java/com/firebolt/jdbc/exception/SQLState.java b/src/main/java/com/firebolt/jdbc/exception/SQLState.java new file mode 100644 index 00000000..62011856 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/exception/SQLState.java @@ -0,0 +1,131 @@ +package com.firebolt.jdbc.exception; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +// https://en.wikipedia.org/wiki/SQLSTATE +public enum SQLState { + SUCCESS("00000"), + WARNING("01000"), + NO_DATA("02000"), + STATEMENT_STRING_DATA_RIGHT_TRUNCATION("01004"), + NULL_VALUE_NO_INDICATOR_PARAMETER("22002"), + CONNECTION_EXCEPTION("08001"), + CONNECTION_DOES_NOT_EXIST("08003"), + CONNECTION_FAILURE("08006"), + TRANSACTION_RESOLUTION_UNKNOWN("08007"), + SQL_SYNTAX_ERROR("42000"), + SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION("42601"), + DUPLICATE_KEY_VALUE("23505"), + DATA_EXCEPTION("22000"), + CHARACTER_NOT_IN_REPERTOIRE("22021"), + STRING_DATA_RIGHT_TRUNCATION("22001"), + NUMERIC_VALUE_OUT_OF_RANGE("22003"), + INVALID_DATETIME_FORMAT("22007"), + INVALID_TIME_ZONE_DISPLACEMENT_VALUE("22009"), + INVALID_ESCAPE_CHARACTER("22019"), + INVALID_PARAMETER_VALUE("22023"), + INVALID_CURSOR_STATE("24000"), + INVALID_TRANSACTION_STATE("25000"), + INVALID_AUTHORIZATION_SPECIFICATION("28000"), + INVALID_SQL_STATEMENT_NAME("26000"), + INVALID_CURSOR_NAME("34000"), + INVALID_SCHEMA_NAME("3F000"), + TRANSACTION_ROLLBACK("40000"), + SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION_IN_DIRECT_STATEMENT("2F000"), + INVALID_SQL_DESCRIPTOR_NAME("33000"), + INVALID_CURSOR_POSITION("34000"), + INVALID_CONDITION_NUMBER("35000"), + INVALID_TRANSACTION_TERMINATION("2D000"), + INVALID_CONNECTION_NAME("2E000"), + INVALID_AUTHORIZATION_NAME("28000"), + INVALID_COLUMN_NAME("42703"), + INVALID_COLUMN_DEFINITION("42P16"), + INVALID_CURSOR_DEFINITION("42P11"), + INVALID_DATABASE_DEFINITION("42P15"), + INVALID_FUNCTION_DEFINITION("42P13"), + INVALID_PREPARED_STATEMENT_DEFINITION("42P12"), + INVALID_SCHEMA_DEFINITION("42P14"), + INVALID_TABLE_DEFINITION("42P01"), + INVALID_OBJECT_DEFINITION("42P17"), + WITH_CHECK_OPTION_VIOLATION("44000"), + INSUFFICIENT_RESOURCES("53000"), + DISK_FULL("53100"), + OUT_OF_MEMORY("53200"), + TOO_MANY_CONNECTIONS("53300"), + CONFIGURATION_LIMIT_EXCEEDED("53400"), + PROGRAM_LIMIT_EXCEEDED("54000"), + OBJECT_NOT_IN_PREREQUISITE_STATE("55000"), + OBJECT_IN_USE("55006"), + CANT_CHANGE_RUNTIME_PARAM("55P02"), + LOCK_NOT_AVAILABLE("55P03"), + OPERATOR_INTERVENTION("57000"), + QUERY_CANCELED("57014"), + ADMIN_SHUTDOWN("57P01"), + CRASH_SHUTDOWN("57P02"), + CANNOT_CONNECT_NOW("57P03"), + DATABASE_DROPPED("57P04"), + SYSTEM_ERROR("58000"), + IO_ERROR("58030"), + UNDEFINED_FILE("58P01"), + DUPLICATE_FILE("58P02"), + SNAPSHOT_TOO_OLD("72000"), + CONFIGURATION_FILE_ERROR("F0000"), + LOCK_FILE_EXISTS("F0001"), + FDW_ERROR("HV000"), + FDW_COLUMN_NAME_NOT_FOUND("HV005"), + FDW_DYNAMIC_PARAMETER_VALUE_NEEDED("HV002"), + FDW_FUNCTION_SEQUENCE_ERROR("HV010"), + FDW_INCONSISTENT_DESCRIPTOR_INFORMATION("HV021"), + FDW_INVALID_ATTRIBUTE_VALUE("HV024"), + FDW_INVALID_COLUMN_NAME("HV007"), + FDW_INVALID_COLUMN_NUMBER("HV008"), + FDW_INVALID_DATA_TYPE("HV004"), + FDW_INVALID_DATA_TYPE_DESCRIPTORS("HV006"), + FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER("HV091"), + FDW_INVALID_HANDLE("HV00B"), + FDW_INVALID_OPTION_INDEX("HV00C"), + FDW_INVALID_OPTION_NAME("HV00D"), + FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH("HV090"), + FDW_INVALID_STRING_FORMAT("HV00A"), + FDW_INVALID_USE_OF_NULL_POINTER("HV009"), + FDW_TOO_MANY_HANDLES("HV014"), + FDW_OUT_OF_MEMORY("HV001"), + FDW_NO_SCHEMAS("HV00P"), + FDW_OPTION_NAME_NOT_FOUND("HV00J"), + FDW_REPLY_HANDLE("HV00K"), + FDW_SCHEMA_NOT_FOUND("HV00Q"), + FDW_TABLE_NOT_FOUND("HV00R"), + FDW_UNABLE_TO_CREATE_EXECUTION("HV00L"), + FDW_UNABLE_TO_CREATE_REPLY("HV00M"), + FDW_UNABLE_TO_ESTABLISH_CONNECTION("HV00N"), + PLPGSQL_ERROR("P0000"), + RAISE_EXCEPTION("P0001"), + NO_DATA_FOUND("P0002"), + TOO_MANY_ROWS("P0003"), + ASSERT_FAILURE("P0004"), + INTERNAL_ERROR("XX000"), + DATA_CORRUPTED("XX001"), + INDEX_CORRUPTED("XX002"), + STATE_NOT_DEFINED(null); + + private final String code; + private static final Map codeMap = new HashMap<>(); + static { + for (SQLState s : EnumSet.allOf(SQLState.class)) + codeMap.put(s.getCode(), s); + } + + SQLState(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public static SQLState fromCode(String sqlState) { + return codeMap.get(sqlState); + } +} diff --git a/src/main/java/com/firebolt/jdbc/exception/ServerError.java b/src/main/java/com/firebolt/jdbc/exception/ServerError.java new file mode 100644 index 00000000..80e75698 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/exception/ServerError.java @@ -0,0 +1,210 @@ +package com.firebolt.jdbc.exception; + +import lombok.Getter; +import lombok.ToString; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toUnmodifiableMap; + +@Getter +@ToString +public class ServerError { + private final Query query; + private final Error[] errors; + + public ServerError(Query query, Error[] errors) { + this.query = query; + this.errors = errors; + } + + public ServerError(JSONObject json) { + this(fromJson(json.optJSONObject("query"), Query::new), fromJson(json.optJSONArray("errors"), Error::new, Error[]::new)); + } + + private static T[] fromJson(JSONArray jsonArray, Function factory, IntFunction arrayFactory) { + return jsonArray == null ? null : IntStream.range(0, jsonArray.length()).boxed().map(jsonArray::getJSONObject).map(factory).toArray(arrayFactory); + } + + private static T fromJson(JSONObject json, Function factory) { + return ofNullable(json).map(factory).orElse(null); + } + + public String getErrorMessage() { + return errors == null ? + null + : + Arrays.stream(errors) + .filter(Objects::nonNull) + .map(e -> Stream.of(e.severity, e.source, e.code, e.name, e.description).filter(Objects::nonNull).map(Object::toString).collect(joining(" "))) + .collect(joining("; ")); + } + + public Query getQuery() { + return query; + } + + public Error[] getErrors() { + return errors; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ServerError that = (ServerError) o; + return Objects.equals(query, that.query) && Objects.deepEquals(errors, that.errors); + } + + @Override + public int hashCode() { + return Objects.hash(query, Arrays.hashCode(errors)); + } + + @Getter + @ToString + public static class Query { + private final String queryId; + private final String requestId; + private final String queryLabel; + + public Query(String queryId, String requestId, String queryLabel) { + this.queryId = queryId; + this.requestId = requestId; + this.queryLabel = queryLabel; + } + + Query(JSONObject json) { + this(json.optString("query_id", null), json.optString("request_id", null), json.optString("query_label", null)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Query query = (Query) o; + return Objects.equals(queryId, query.queryId) && Objects.equals(requestId, query.requestId) && Objects.equals(queryLabel, query.queryLabel); + } + + @Override + public int hashCode() { + return Objects.hash(queryId, requestId, queryLabel); + } + } + + @Getter + @ToString + public static class Error { + private final String code; + private final String name; + private final Severity severity; + private final Source source; + private final String description; + private final String resolution; + private final String helpLink; + private final Location location; + + @SuppressWarnings("java:S107") // the price of the immutability + public Error(String code, String name, Severity severity, Source source, String description, String resolution, String helpLink, Location location) { + this.code = code; + this.name = name; + this.severity = severity; + this.source = source; + this.description = description; + this.resolution = resolution; + this.helpLink = helpLink; + this.location = location; + } + + Error(JSONObject json) { + this(json.optString("code", null), json.optString("name", null), + json.optEnum(Severity.class, "severity"), + ofNullable(json.optString("source", null)).map(Source::fromText).orElse(Source.UNKNOWN), + json.optString("description", null), json.optString("resolution", null), json.optString("helpLink", null), + ofNullable(json.optJSONObject("location", null)).map(Location::new).orElse(null)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Error error = (Error) o; + return Objects.equals(code, error.code) && Objects.equals(name, error.name) && severity == error.severity && source == error.source && Objects.equals(description, error.description) && Objects.equals(resolution, error.resolution) && Objects.equals(helpLink, error.helpLink) && Objects.equals(location, error.location); + } + + @Override + public int hashCode() { + return Objects.hash(code, name, severity, source, description, resolution, helpLink, location); + } + + public enum Severity { + ERROR, WARNING, + } + + public enum Source { + SYSTEM_ERROR("System Error"), + USER_ERROR("User Error"), + UNKNOWN("Unknown"), + USER_WARNING("User Warning"), + SYSTEM_WARNING("System Warning"), + SYSTEM_SEVIER_WARNING("System Sevier Warning"), + ; + private final String text; + private static final Map textToSource = Arrays.stream(values()).collect(toUnmodifiableMap(e -> e.text, e -> e)); + + Source(String text) { + this.text = text; + } + + public static Source fromText(String text) { + return textToSource.get(text); + } + + @Override + public String toString() { + return text; + } + } + + @Getter + @ToString + public static class Location { + private final int failingLine; + private final int startOffset; + private final int endOffset; + + public Location(int failingLine, int startOffset, int endOffset) { + this.failingLine = failingLine; + this.startOffset = startOffset; + this.endOffset = endOffset; + } + + Location(JSONObject json) { + this(json.optInt("failingLine"), json.optInt("startOffset"), json.optInt("endOffset")); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Location location = (Location) o; + return failingLine == location.failingLine && startOffset == location.startOffset && endOffset == location.endOffset; + } + + @Override + public int hashCode() { + return Objects.hash(failingLine, startOffset, endOffset); + } + } + } +} diff --git a/src/main/java/com/firebolt/jdbc/log/FireboltLogger.java b/src/main/java/com/firebolt/jdbc/log/FireboltLogger.java deleted file mode 100644 index 478c4548..00000000 --- a/src/main/java/com/firebolt/jdbc/log/FireboltLogger.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.firebolt.jdbc.log; - -public interface FireboltLogger { - - void trace(String message); - - void trace(String message, Object... arguments); - - void trace(String message, Throwable t); - - void debug(String message); - - void debug(String message, Object... arguments); - - void debug(String message, Throwable t); - - void info(String message); - - void info(String message, Object... arguments); - - void info(String message, Throwable t); - - void warn(String message); - - void warn(String message, Object... arguments); - - void warn(String message, Throwable t); - - void error(String message); - - void error(String message, Object... arguments); - - void error(String message, Throwable t); - -} diff --git a/src/main/java/com/firebolt/jdbc/log/JDKLogger.java b/src/main/java/com/firebolt/jdbc/log/JDKLogger.java deleted file mode 100644 index ad19f26d..00000000 --- a/src/main/java/com/firebolt/jdbc/log/JDKLogger.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.firebolt.jdbc.log; - -import java.util.logging.Level; - -public class JDKLogger implements FireboltLogger { - - private final java.util.logging.Logger logger; - - public JDKLogger(String name) { - this.logger = java.util.logging.Logger.getLogger(name); - } - - @Override - public void trace(String message) { - logger.log(Level.FINEST, message); - } - - @Override - public void trace(String message, Object... arguments) { - logger.log(Level.FINEST, addMissingArgumentsIndexes(message), arguments); - } - - @Override - public void trace(String message, Throwable t) { - logger.log(Level.FINEST, message, t); - } - - @Override - public void debug(String message) { - logger.log(Level.FINE, message); - } - - @Override - public void debug(String message, Object... arguments) { - logger.log(Level.FINE, addMissingArgumentsIndexes(message), arguments); - } - - @Override - public void debug(String message, Throwable t) { - logger.log(Level.FINE, message, t); - } - - @Override - public void info(String message) { - logger.log(Level.INFO, message); - } - - @Override - public void info(String message, Object... arguments) { - logger.log(Level.INFO, addMissingArgumentsIndexes(message), arguments); - } - - @Override - public void info(String message, Throwable t) { - logger.log(Level.INFO, message, t); - } - - @Override - public void warn(String message) { - logger.log(Level.WARNING, message); - } - - @Override - public void warn(String message, Object... arguments) { - logger.log(Level.WARNING, addMissingArgumentsIndexes(message), arguments); - } - - @Override - public void warn(String message, Throwable t) { - logger.log(Level.WARNING, message, t); - - } - - @Override - public void error(String message) { - logger.log(Level.SEVERE, message); - } - - @Override - public void error(String message, Object... arguments) { - logger.log(Level.SEVERE, addMissingArgumentsIndexes(message), arguments); - } - - @Override - public void error(String message, Throwable t) { - logger.log(Level.SEVERE, message, t); - } - - /** - * SLF4J and java.util.logging use a different log format. With SLF4J it is not - * required to have argument indexes in the logs (eg: "log.info("hello {}", - * "world");), but it is required for java.util.logging (eg: "log.info("hello - * {1}", "world");) In this project we use the SLF4J way of logging, which is - * why we need to add the missing indexes. - */ - private String addMissingArgumentsIndexes(String message) { - StringBuilder result = new StringBuilder(); - int argumentIndex = 0; - int i = 0; - while (i < message.length()) { - if (message.charAt(i) == '{' && i < message.length() - 1 && message.charAt(i + 1) == '}') { - result.append(String.format("{%d}", argumentIndex++)); - i++; - } else { - result.append(message.charAt(i)); - } - i++; - } - return result.toString(); - } -} diff --git a/src/main/java/com/firebolt/jdbc/log/SLF4JLogger.java b/src/main/java/com/firebolt/jdbc/log/SLF4JLogger.java deleted file mode 100644 index ba769bbf..00000000 --- a/src/main/java/com/firebolt/jdbc/log/SLF4JLogger.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.firebolt.jdbc.log; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class SLF4JLogger implements FireboltLogger { - - private final Logger logger; - - public SLF4JLogger(String name) { - logger = LoggerFactory.getLogger(name); - } - - @Override - public void trace(String message) { - logger.trace(message); - } - - @Override - public void trace(String message, Object... arguments) { - logger.trace(message, arguments); - } - - @Override - public void trace(String message, Throwable t) { - logger.trace(message, t); - } - - @Override - public void debug(String message) { - logger.debug(message); - } - - @Override - public void debug(String message, Object... arguments) { - logger.debug(message, arguments); - - } - - @Override - public void debug(String message, Throwable t) { - logger.debug(message, t); - } - - @Override - public void info(String message) { - logger.info(message); - } - - @Override - public void info(String message, Object... arguments) { - logger.info(message, arguments); - } - - @Override - public void info(String message, Throwable t) { - logger.info(message, t); - } - - @Override - public void warn(String message) { - logger.warn(message); - } - - @Override - public void warn(String message, Object... arguments) { - logger.warn(message, arguments); - } - - @Override - public void warn(String message, Throwable t) { - logger.warn(message, t); - } - - @Override - public void error(String message) { - logger.error(message); - } - - @Override - public void error(String message, Object... arguments) { - logger.error(message, arguments); - } - - @Override - public void error(String message, Throwable t) { - logger.error(message, t); - } -} diff --git a/src/main/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadata.java b/src/main/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadata.java index 1f6eaa2d..5a7abbac 100644 --- a/src/main/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadata.java +++ b/src/main/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadata.java @@ -1,38 +1,164 @@ package com.firebolt.jdbc.metadata; +import com.firebolt.jdbc.GenericWrapper; import com.firebolt.jdbc.QueryResult; -import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; -import com.firebolt.jdbc.annotation.NotImplemented; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.resultset.column.Column; import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.util.VersionUtil; -import lombok.CustomLog; -import org.apache.commons.lang3.StringUtils; -import java.sql.*; -import java.util.*; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.JDBCType; +import java.sql.ResultSet; +import java.sql.RowIdLifetime; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; -import static com.firebolt.jdbc.metadata.MetadataColumns.*; -import static com.firebolt.jdbc.type.FireboltDataType.*; +import static com.firebolt.jdbc.metadata.MetadataColumns.ASC_OR_DESC; +import static com.firebolt.jdbc.metadata.MetadataColumns.ATTR_DEF; +import static com.firebolt.jdbc.metadata.MetadataColumns.ATTR_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.ATTR_SIZE; +import static com.firebolt.jdbc.metadata.MetadataColumns.ATTR_TYPE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.AUTO_INCREMENT; +import static com.firebolt.jdbc.metadata.MetadataColumns.BASE_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.BUFFER_LENGTH; +import static com.firebolt.jdbc.metadata.MetadataColumns.CARDINALITY; +import static com.firebolt.jdbc.metadata.MetadataColumns.CASE_SENSITIVE; +import static com.firebolt.jdbc.metadata.MetadataColumns.CHAR_OCTET_LENGTH; +import static com.firebolt.jdbc.metadata.MetadataColumns.CLASS_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_DEF; +import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_SIZE; +import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_USAGE; +import static com.firebolt.jdbc.metadata.MetadataColumns.COMMON_RADIX; +import static com.firebolt.jdbc.metadata.MetadataColumns.CREATE_PARAMS; +import static com.firebolt.jdbc.metadata.MetadataColumns.DATA_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.DECIMAL_DIGITS; +import static com.firebolt.jdbc.metadata.MetadataColumns.DEFAULT_VALUE; +import static com.firebolt.jdbc.metadata.MetadataColumns.DEFERRABILITY; +import static com.firebolt.jdbc.metadata.MetadataColumns.DELETE_RULE; +import static com.firebolt.jdbc.metadata.MetadataColumns.DESCRIPTION; +import static com.firebolt.jdbc.metadata.MetadataColumns.FILTER_CONDITION; +import static com.firebolt.jdbc.metadata.MetadataColumns.FIXED_PREC_SCALE; +import static com.firebolt.jdbc.metadata.MetadataColumns.FKCOLUMN_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.FKTABLE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.FKTABLE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.FKTABLE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.FK_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.FUNCTION_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.FUNCTION_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.FUNCTION_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.FUNCTION_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.GRANTEE; +import static com.firebolt.jdbc.metadata.MetadataColumns.GRANTOR; +import static com.firebolt.jdbc.metadata.MetadataColumns.INDEX_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.INDEX_QUALIFIER; +import static com.firebolt.jdbc.metadata.MetadataColumns.IS_AUTOINCREMENT; +import static com.firebolt.jdbc.metadata.MetadataColumns.IS_GENERATEDCOLUMN; +import static com.firebolt.jdbc.metadata.MetadataColumns.IS_GRANTABLE; +import static com.firebolt.jdbc.metadata.MetadataColumns.IS_NULLABLE; +import static com.firebolt.jdbc.metadata.MetadataColumns.KEY_SEQ; +import static com.firebolt.jdbc.metadata.MetadataColumns.LENGTH; +import static com.firebolt.jdbc.metadata.MetadataColumns.LITERAL_PREFIX; +import static com.firebolt.jdbc.metadata.MetadataColumns.LITERAL_SUFFIX; +import static com.firebolt.jdbc.metadata.MetadataColumns.LOCAL_TYPE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.MAXIMUM_SCALE; +import static com.firebolt.jdbc.metadata.MetadataColumns.MAX_LEN; +import static com.firebolt.jdbc.metadata.MetadataColumns.MINIMUM_SCALE; +import static com.firebolt.jdbc.metadata.MetadataColumns.NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.NON_UNIQUE; +import static com.firebolt.jdbc.metadata.MetadataColumns.NULLABLE; +import static com.firebolt.jdbc.metadata.MetadataColumns.NUM_PREC_RADIX; +import static com.firebolt.jdbc.metadata.MetadataColumns.ORDINAL_POSITION; +import static com.firebolt.jdbc.metadata.MetadataColumns.PAGES; +import static com.firebolt.jdbc.metadata.MetadataColumns.PKCOLUMN_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.PKTABLE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.PKTABLE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.PKTABLE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.PK_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.PRECISION; +import static com.firebolt.jdbc.metadata.MetadataColumns.PRIVILEGE; +import static com.firebolt.jdbc.metadata.MetadataColumns.PROCEDURE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.PROCEDURE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.PROCEDURE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.PROCEDURE_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.PSEUDO_COLUMN; +import static com.firebolt.jdbc.metadata.MetadataColumns.RADIX; +import static com.firebolt.jdbc.metadata.MetadataColumns.REF_GENERATION; +import static com.firebolt.jdbc.metadata.MetadataColumns.REMARKS; +import static com.firebolt.jdbc.metadata.MetadataColumns.SCALE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SCOPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SCOPE_CATALOG; +import static com.firebolt.jdbc.metadata.MetadataColumns.SCOPE_SCHEMA; +import static com.firebolt.jdbc.metadata.MetadataColumns.SCOPE_TABLE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SEARCHABLE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SELF_REFERENCING_COL_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.SOURCE_DATA_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SPECIFIC_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.SQL_DATA_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.SQL_DATETIME_SUB; +import static com.firebolt.jdbc.metadata.MetadataColumns.SUPERTABLE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.SUPERTYPE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.SUPERTYPE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.SUPERTYPE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_CATALOG; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE; +import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_CAT; +import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_NAME; +import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_SCHEM; +import static com.firebolt.jdbc.metadata.MetadataColumns.UNSIGNED_ATTRIBUTE; +import static com.firebolt.jdbc.metadata.MetadataColumns.UPDATE_RULE; +import static com.firebolt.jdbc.type.FireboltDataType.ARRAY; +import static com.firebolt.jdbc.type.FireboltDataType.BIG_INT; +import static com.firebolt.jdbc.type.FireboltDataType.BOOLEAN; +import static com.firebolt.jdbc.type.FireboltDataType.BYTEA; +import static com.firebolt.jdbc.type.FireboltDataType.DATE; +import static com.firebolt.jdbc.type.FireboltDataType.DOUBLE_PRECISION; +import static com.firebolt.jdbc.type.FireboltDataType.INTEGER; +import static com.firebolt.jdbc.type.FireboltDataType.NUMERIC; +import static com.firebolt.jdbc.type.FireboltDataType.REAL; +import static com.firebolt.jdbc.type.FireboltDataType.TEXT; +import static com.firebolt.jdbc.type.FireboltDataType.TIMESTAMP; +import static com.firebolt.jdbc.type.FireboltDataType.TUPLE; +import static java.lang.String.format; +import static java.sql.ResultSet.TYPE_FORWARD_ONLY; import static java.sql.Types.VARCHAR; +import static java.util.Map.entry; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.compile; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; -import static org.apache.commons.lang3.StringUtils.containsIgnoreCase; -@CustomLog -public class FireboltDatabaseMetadata implements DatabaseMetaData { +@SuppressWarnings("java:S6204") // compatibility with JDK 11 +public class FireboltDatabaseMetadata implements DatabaseMetaData, GenericWrapper { - private static final String PUBLIC_SCHEMA_NAME = "public"; - private static final String INFORMATION_SCHEMA_NAME = "information_schema"; - private static final String CATALOG_SCHEMA_NAME = "catalog"; + private static final String TABLE = "TABLE"; + private static final String VIEW = "VIEW"; private static final String QUOTE = "'"; private static final int MAX_IDENTIFIER_LENGTH = 63; + private static final int MAX_LITERAL_LENGTH = 0x40000; // 262144 + private final String url; private final FireboltConnection connection; - private String databaseVersion; + private volatile String databaseVersion; public FireboltDatabaseMetadata(String url, FireboltConnection connection) { this.url = url; @@ -46,28 +172,34 @@ public ResultSet getSchemas() throws SQLException { @Override public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { - String dbName = connection.getSessionProperties().getDatabase(); - List publicRow = Arrays.asList(PUBLIC_SCHEMA_NAME, dbName); - List informationSchemaRow = Arrays.asList(INFORMATION_SCHEMA_NAME, dbName); - List catalogRow = Arrays.asList(CATALOG_SCHEMA_NAME, dbName); - return FireboltResultSet.of(QueryResult.builder() - .columns(Arrays.asList(QueryResult.Column.builder().name(TABLE_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_CATALOG).type(TEXT).build())) - .rows(Arrays.asList(publicRow, informationSchemaRow, catalogRow)).build()); + String catalogClause = catalog == null ? null : format("%s LIKE '%s'", TABLE_CATALOG, catalog); + String schemaClause = schemaPattern == null ? null : format("TABLE_SCHEMA LIKE '%s'", schemaPattern); + String where = Stream.of(catalogClause, schemaClause).filter(Objects::nonNull).collect(joining(" AND ")); + if (!where.isEmpty()) { + where = " WHERE " + where; + } + return getSchemas("SELECT DISTINCT TABLE_SCHEMA AS TABLE_SCHEM, TABLE_CATALOG FROM information_schema.tables" + where); + } + + private ResultSet getSchemas(String query) throws SQLException { + List> rows = new ArrayList<>(); + try (Statement statement = connection.createStatement(); + ResultSet schemaDescription = statement.executeQuery(query)) { + while (schemaDescription.next()) { + rows.add(List.of(schemaDescription.getString(TABLE_SCHEM), schemaDescription.getString(TABLE_CATALOG))); + } + } + return createResultSet(Stream.of(entry(TABLE_SCHEM, TEXT), entry(TABLE_CATALOG, TEXT)), rows); } @Override public ResultSet getTableTypes() throws SQLException { - return FireboltResultSet.of(QueryResult.builder() - .columns(List.of(QueryResult.Column.builder().name(TABLE_TYPE).type(TEXT).build())) - .rows(List.of(List.of("TABLE"), List.of("VIEW"))).build()); + return createResultSet(Stream.of(entry(TABLE_TYPE, TEXT)), List.of(List.of(TABLE), List.of(VIEW))); } @Override public ResultSet getCatalogs() throws SQLException { - return FireboltResultSet.of(QueryResult.builder() - .columns(Collections.singletonList(QueryResult.Column.builder().name(TABLE_CAT).type(TEXT).build())) - .rows(Collections.singletonList(Collections.singletonList(connection.getCatalog()))).build()); + return createResultSet(Stream.of(entry(TABLE_CAT, TEXT)), List.of(List.of(connection.getCatalog()))); } @Override @@ -76,63 +208,37 @@ public Connection getConnection() throws SQLException { } @Override - public String getDatabaseProductName() throws SQLException { + public String getDatabaseProductName() { return "Firebolt"; } @Override - public String getURL() throws SQLException { + public String getURL() { return url; } @Override - public String getDriverName() throws SQLException { + public String getDriverName() { return "Firebolt JDBC Driver"; } @Override - public boolean supportsTransactionIsolationLevel(int level) throws SQLException { + public boolean supportsTransactionIsolationLevel(int level) { return level == Connection.TRANSACTION_NONE; } @Override public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { - List columns = Arrays.asList( - QueryResult.Column.builder().name(TABLE_CAT).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(COLUMN_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(TYPE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(COLUMN_SIZE).type(INTEGER).build(), - QueryResult.Column.builder().name(BUFFER_LENGTH).type(INTEGER).build(), - QueryResult.Column.builder().name(DECIMAL_DIGITS).type(INTEGER).build(), - QueryResult.Column.builder().name(NUM_PREC_RADIX).type(INTEGER).build(), - QueryResult.Column.builder().name(NULLABLE).type(INTEGER).build(), - QueryResult.Column.builder().name(REMARKS).type(TEXT).build(), - QueryResult.Column.builder().name(COLUMN_DEF).type(TEXT).build(), - QueryResult.Column.builder().name(SQL_DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(SQL_DATETIME_SUB).type(INTEGER).build(), - QueryResult.Column.builder().name(CHAR_OCTET_LENGTH).type(INTEGER).build(), - QueryResult.Column.builder().name(ORDINAL_POSITION).type(INTEGER).build(), - QueryResult.Column.builder().name(IS_NULLABLE).type(TEXT).build(), - QueryResult.Column.builder().name(SCOPE_CATALOG).type(TEXT).build(), - QueryResult.Column.builder().name(SCOPE_SCHEMA).type(TEXT).build(), - QueryResult.Column.builder().name(SCOPE_TABLE).type(TEXT).build(), - QueryResult.Column.builder().name(SOURCE_DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(IS_AUTOINCREMENT).type(TEXT).build(), - QueryResult.Column.builder().name(IS_GENERATEDCOLUMN).type(TEXT).build()); - List> rows = new ArrayList<>(); String query = MetadataUtil.getColumnsQuery(schemaPattern, tableNamePattern, columnNamePattern); - try (Statement statement = this.connection.createStatement(); + try (Statement statement = connection.createStatement(); ResultSet columnDescription = statement.executeQuery(query)) { while (columnDescription.next()) { - List row; Column columnInfo = Column.of(columnDescription.getString("data_type"), columnDescription.getString("column_name")); - row = Arrays.asList(connection.getCatalog(), // TABLE_CAT + String columnDefault = columnDescription.getString("column_default"); + rows.add(Arrays.asList(connection.getCatalog(), // TABLE_CAT columnDescription.getString("table_schema"), // schema columnDescription.getString("table_name"), // table name columnDescription.getString("column_name"), // column name @@ -144,14 +250,12 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa String.valueOf(COMMON_RADIX), // radix isColumnNullable(columnDescription) ? columnNullable : columnNoNulls, null, // description of the column - StringUtils.isNotBlank(columnDescription.getString("column_default")) - ? columnDescription.getString("column_default") - : null, // default value for the column: null, + columnDefault == null || columnDefault.isBlank() ? null : columnDefault, null, // SQL_DATA_TYPE - reserved for future use (see javadoc) null, // SQL_DATETIME_SUB - reserved for future use (see javadoc) null, // CHAR_OCTET_LENGTH - The maximum - // length of binary and character - // based columns (null for others) + // length of binary and character + // based columns (null for others) columnDescription.getInt("ordinal_position"), // The ordinal position starting from 1 isColumnNullable(columnDescription) ? "YES" : "NO", null, // "SCOPE_CATALOG - Unused @@ -159,66 +263,79 @@ public ResultSet getColumns(String catalog, String schemaPattern, String tableNa null, // "SCOPE_TABLE" - Unused null, // "SOURCE_DATA_TYPE" - Unused "NO", // IS_AUTOINCREMENT - Not supported - "NO"); // IS_GENERATEDCOLUMN - Not supported - rows.add(row); + "NO")); // IS_GENERATEDCOLUMN - Not supported } - return FireboltResultSet.of(QueryResult.builder().rows(rows).columns(columns).build()); + return createResultSet( + Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(TYPE_NAME, TEXT), + entry(COLUMN_SIZE, INTEGER), + entry(BUFFER_LENGTH, INTEGER), + entry(DECIMAL_DIGITS, INTEGER), + entry(NUM_PREC_RADIX, INTEGER), + entry(NULLABLE, INTEGER), + entry(REMARKS, TEXT), + entry(COLUMN_DEF, TEXT), + entry(SQL_DATA_TYPE, INTEGER), + entry(SQL_DATETIME_SUB, INTEGER), + entry(CHAR_OCTET_LENGTH, INTEGER), + entry(ORDINAL_POSITION, INTEGER), + entry(IS_NULLABLE, TEXT), + entry(SCOPE_CATALOG, TEXT), + entry(SCOPE_SCHEMA, TEXT), + entry(SCOPE_TABLE, TEXT), + entry(SOURCE_DATA_TYPE, INTEGER), + entry(IS_AUTOINCREMENT, TEXT), + entry(IS_GENERATEDCOLUMN, TEXT)), + rows); } } private boolean isColumnNullable(ResultSet columnDescription) throws SQLException { + final String isNullable = "is_nullable"; try { - return columnDescription.getInt("is_nullable") == 1; - } catch (Exception e) { - return columnDescription.getBoolean("is_nullable"); + return columnDescription.getInt(isNullable) == 1; + } catch (SQLException | NumberFormatException e1) { + try { + return columnDescription.getBoolean(isNullable); + } catch (SQLException e2) { + return "YES".equals(columnDescription.getString(isNullable)); + } } } @Override - public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr) - throws SQLException { - List> rows = Stream - .of(this.getTables(catalog, schemaPattern, tableNamePattern, typesArr, false), - this.getTables(catalog, schemaPattern, tableNamePattern, typesArr, true)) - .flatMap(Collection::stream).collect(toList()); + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr) throws SQLException { + return createResultSet( + Stream.of(TABLE_CAT, TABLE_SCHEM, TABLE_NAME, TABLE_TYPE, REMARKS, TYPE_CAT, TYPE_SCHEM, TYPE_NAME, SELF_REFERENCING_COL_NAME, REF_GENERATION) + .map(name -> entry(name, TEXT)), + getTablesData(catalog, schemaPattern, tableNamePattern, typesArr)); + } - return FireboltResultSet.of(QueryResult.builder() - .columns(Arrays.asList(QueryResult.Column.builder().name(TABLE_CAT).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(TABLE_TYPE).type(TEXT).build(), - QueryResult.Column.builder().name(REMARKS).type(TEXT).build(), - QueryResult.Column.builder().name(TYPE_CAT).type(TEXT).build(), - QueryResult.Column.builder().name(TYPE_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(TYPE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(SELF_REFERENCING_COL_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(REF_GENERATION).type(TEXT).build())) - .rows(rows).build()); - } - - private List> getTables(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr, - boolean isView) throws SQLException { + private List> getTablesData(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr) throws SQLException { + Set types = typesArr == null ? Set.of(TABLE, VIEW) : Set.of(typesArr); + List tableTypes = List.of("BASE TABLE", "DIMENSION", "FACT"); + List trulyTableTypes = new ArrayList<>(); + if (types.contains(TABLE)) { + trulyTableTypes.addAll(tableTypes); + } + if (types.contains(VIEW)) { + trulyTableTypes.add(VIEW); + } + String query = MetadataUtil.getTablesQuery(catalog, schemaPattern, tableNamePattern, trulyTableTypes.toArray(new String[0])); List> rows = new ArrayList<>(); - - String query = isView ? MetadataUtil.getViewsQuery(catalog, schemaPattern, tableNamePattern) - : MetadataUtil.getTablesQuery(catalog, schemaPattern, tableNamePattern); - try (Statement statement = this.connection.createStatement(); - ResultSet tables = statement.executeQuery(query)) { - - Set types = typesArr != null ? new HashSet<>(Arrays.asList(typesArr)) : null; + try (Statement statement = connection.createStatement(); ResultSet tables = statement.executeQuery(query)) { while (tables.next()) { - List row = new ArrayList<>(); - row.add(connection.getCatalog()); - row.add(tables.getString("table_schema")); - row.add(tables.getString("table_name")); - String tableType = isView ? "VIEW" : "TABLE"; - row.add(tableType); - for (int i = 3; i < 9; i++) { - row.add(null); - } - if (types == null || types.contains(tableType)) { - rows.add(row); - } + String tableType = tables.getString("table_type"); + tableType = tableTypes.contains(tableType) ? TABLE: tableType; // replace FACT and DIMENSION by TABLE + List row = Arrays.asList( + connection.getCatalog(), tables.getString("table_schema"), tables.getString("table_name"), tableType, + null, null, null, null, null, null); + rows.add(row); } } return rows; @@ -226,55 +343,54 @@ private List> getTables(String catalog, String schemaPattern, String tab @Override public ResultSet getTypeInfo() throws SQLException { - List columns = Arrays.asList( - QueryResult.Column.builder().name(TYPE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(PRECISION).type(INTEGER).build(), - QueryResult.Column.builder().name(LITERAL_PREFIX).type(TEXT).build(), - QueryResult.Column.builder().name(LITERAL_SUFFIX).type(TEXT).build(), - QueryResult.Column.builder().name(CREATE_PARAMS).type(TEXT).build(), - QueryResult.Column.builder().name(NULLABLE).type(INTEGER).build(), - QueryResult.Column.builder().name(CASE_SENSITIVE).type(BOOLEAN).build(), - QueryResult.Column.builder().name(SEARCHABLE).type(INTEGER).build(), - QueryResult.Column.builder().name(UNSIGNED_ATTRIBUTE).type(BOOLEAN).build(), - QueryResult.Column.builder().name(FIXED_PREC_SCALE).type(BOOLEAN).build(), - QueryResult.Column.builder().name(AUTO_INCREMENT).type(BOOLEAN).build(), - QueryResult.Column.builder().name(LOCAL_TYPE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(MINIMUM_SCALE).type(INTEGER).build(), - QueryResult.Column.builder().name(MAXIMUM_SCALE).type(INTEGER).build(), - QueryResult.Column.builder().name(SQL_DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(SQL_DATETIME_SUB).type(INTEGER).build(), - QueryResult.Column.builder().name(NUM_PREC_RADIX).type(INTEGER).build()); - - List> rows = new ArrayList<>(); List usableTypes = Arrays.asList(INTEGER, BIG_INT, REAL, DOUBLE_PRECISION, TEXT, DATE, TIMESTAMP, NUMERIC, ARRAY, TUPLE, BYTEA, BOOLEAN); - usableTypes - .forEach( - type -> rows.add(Arrays.asList(type.getDisplayName(), type.getSqlType(), - type.getPrecision(), QUOTE, // LITERAL_PREFIX - QUOTE, // LITERAL_SUFFIX - null, // Description of the creation parameters - can be null (can set if needed - // in the future) - typeNullableUnknown, // It depends - A type can be nullable or not depending on - // the presence of the additional keyword Nullable() - type.isCaseSensitive(), type.getSqlType() == VARCHAR ? typeSearchable - : typePredBasic, /* - * SEARCHABLE - LIKE can only be used for VARCHAR - */ - !type.isSigned(), - false, // FIXED_PREC_SCALE - indicates if the type can be a money value. - // Always - // false as we do not have a money type - false, // AUTO_INCREMENT - null, // LOCAL_TYPE_NAME - type.getMinScale(), // MINIMUM_SCALE - type.getMaxScale(), // MAXIMUM_SCALE - null, // SQL_DATA_TYPE - Not needed - reserved for future use - null, // SQL_DATETIME_SUB - Not needed - reserved for future use - COMMON_RADIX))); - - return FireboltResultSet.of(QueryResult.builder().columns(columns).rows(rows).build()); + List> rows = usableTypes.stream().map(type -> + Arrays.asList(type.getDisplayName(), type.getSqlType(), + type.getPrecision(), QUOTE, // LITERAL_PREFIX + QUOTE, // LITERAL_SUFFIX + null, // Description of the creation parameters - can be null (can set if needed + // in the future) + typeNullableUnknown, // It depends - A type can be nullable or not depending on + // the presence of the additional keyword Nullable() + type.isCaseSensitive(), type.getSqlType() == VARCHAR ? typeSearchable + : typePredBasic, /* + * SEARCHABLE - LIKE can only be used for VARCHAR + */ + !type.isSigned(), + false, // FIXED_PREC_SCALE - indicates if the type can be a money value. + // Always + // false as we do not have a money type + false, // AUTO_INCREMENT + null, // LOCAL_TYPE_NAME + type.getMinScale(), // MINIMUM_SCALE + type.getMaxScale(), // MAXIMUM_SCALE + null, // SQL_DATA_TYPE - Not needed - reserved for future use + null, // SQL_DATETIME_SUB - Not needed - reserved for future use + COMMON_RADIX)) + .collect(toList()); + + return createResultSet( + Stream.of( + entry(TYPE_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(PRECISION, INTEGER), + entry(LITERAL_PREFIX, TEXT), + entry(LITERAL_SUFFIX, TEXT), + entry(CREATE_PARAMS, TEXT), + entry(NULLABLE, INTEGER), + entry(CASE_SENSITIVE, BOOLEAN), + entry(SEARCHABLE, INTEGER), + entry(UNSIGNED_ATTRIBUTE, BOOLEAN), + entry(FIXED_PREC_SCALE, BOOLEAN), + entry(AUTO_INCREMENT, BOOLEAN), + entry(LOCAL_TYPE_NAME, TEXT), + entry(MINIMUM_SCALE, INTEGER), + entry(MAXIMUM_SCALE, INTEGER), + entry(SQL_DATA_TYPE, INTEGER), + entry(SQL_DATETIME_SUB, INTEGER), + entry(NUM_PREC_RADIX, INTEGER)), + rows); } @Override @@ -289,16 +405,12 @@ public int getDriverMinorVersion() { @Override public String getDatabaseProductVersion() throws SQLException { - if (this.databaseVersion == null) { - String engine = this.connection.getEngine(); - try (Statement statement = this.connection.createStatement()) { - String query = MetadataUtil.getDatabaseVersionQuery(engine); - ResultSet rs = statement.executeQuery(query); - rs.next(); - this.databaseVersion = rs.getString(1); + if (databaseVersion == null) { + try (Statement statement = connection.createStatement(); ResultSet rs = statement.executeQuery("SELECT VERSION()")) { + databaseVersion = rs.next() ? rs.getString(1) : ""; } } - return this.databaseVersion; + return databaseVersion; } @Override @@ -312,49 +424,33 @@ public int getDatabaseMinorVersion() throws SQLException { } @Override - public int getJDBCMajorVersion() throws SQLException { + public int getJDBCMajorVersion() { return VersionUtil.extractMajorVersion(VersionUtil.getSpecificationVersion()); } @Override - public int getJDBCMinorVersion() throws SQLException { + public int getJDBCMinorVersion() { return VersionUtil.extractMinorVersion(VersionUtil.getSpecificationVersion()); } @Override - public String getDriverVersion() throws SQLException { + public String getDriverVersion() { return VersionUtil.getDriverVersion(); } @Override - public boolean isWrapperFor(Class iface) throws SQLException { - return iface.isAssignableFrom(getClass()); - } - - @Override - public T unwrap(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return iface.cast(this); - } - throw new SQLException("Cannot unwrap to " + iface.getName()); - } - - @Override - @ExcludeFromJacocoGeneratedReport - public boolean allProceduresAreCallable() throws SQLException { - return true; + public boolean allProceduresAreCallable() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean allTablesAreSelectable() throws SQLException { + public boolean allTablesAreSelectable() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public String getUserName() throws SQLException { - return connection.getSessionProperties().getUser(); + public String getUserName() { + return connection.getSessionProperties().getPrincipal(); } @Override @@ -363,680 +459,577 @@ public boolean isReadOnly() throws SQLException { } @Override - @ExcludeFromJacocoGeneratedReport - public boolean nullsAreSortedHigh() throws SQLException { - return true; + public boolean nullsAreSortedHigh() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean nullsAreSortedLow() throws SQLException { + public boolean nullsAreSortedLow() { return !nullsAreSortedHigh(); } @Override - @ExcludeFromJacocoGeneratedReport - public boolean nullsAreSortedAtStart() throws SQLException { - return true; + public boolean nullsAreSortedAtStart() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean nullsAreSortedAtEnd() throws SQLException { + public boolean nullsAreSortedAtEnd() { return !nullsAreSortedAtStart(); } @Override - @ExcludeFromJacocoGeneratedReport - public boolean usesLocalFiles() throws SQLException { + public boolean usesLocalFiles() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean usesLocalFilePerTable() throws SQLException { + public boolean usesLocalFilePerTable() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMixedCaseIdentifiers() throws SQLException { - return true; + public boolean supportsMixedCaseIdentifiers() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesUpperCaseIdentifiers() throws SQLException { + public boolean storesUpperCaseIdentifiers() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesLowerCaseIdentifiers() throws SQLException { - return false; + public boolean storesLowerCaseIdentifiers() { + return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesMixedCaseIdentifiers() throws SQLException { - return true; + public boolean storesMixedCaseIdentifiers() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMixedCaseQuotedIdentifiers() throws SQLException { + public boolean supportsMixedCaseQuotedIdentifiers() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesUpperCaseQuotedIdentifiers() throws SQLException { + public boolean storesUpperCaseQuotedIdentifiers() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesLowerCaseQuotedIdentifiers() throws SQLException { + public boolean storesLowerCaseQuotedIdentifiers() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean storesMixedCaseQuotedIdentifiers() throws SQLException { + public boolean storesMixedCaseQuotedIdentifiers() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public String getIdentifierQuoteString() throws SQLException { + public String getIdentifierQuoteString() { return "\""; } @Override public String getSQLKeywords() { // Firebolt reserved words minus SQL:2003 keywords - return "BOOL,CONCAT,COPY,DATABASE,DATETIME,DOUBLECOLON,DOW," - + "DOY,EMPTY_IDENTIFIER,EPOCH,EXPLAIN,EXTRACT,FIRST,GENERATE,ILIKE,ISNULL," - + "JOIN_TYPE,LIMIT,LIMIT_DISTINCT,LONG,NEXT,OFFSET,PRIMARY,QUARTER,SAMPLE,SHOW,TEXT," - + "TOP,TRIM,TRUNCATE,UNKNOWN_CHAR,UNTERMINATED_STRING,WEEK"; + return "ACCOUNT,AGGREGATING,ALTER,AS,ATTACH,CACHE,CANCEL,COLUMNS,COPY,CREATE,DATABASE,DATABASES,DELETE,DESCRIBE,DROP," + + "ENGINE,ENGINES,EXTERNAL,GRANT,INDEX,INDEXES,INSERT,LOGIN,NETWORK,ORGANIZATION,POLICY,QUERY,REVOKE,ROLE," + + "SELECT,SERVICE,SHOW,START,STOP,TABLE,TABLES,TO,TRUNCATE,UPDATE,USER,VACUUM,VIEW,VIEWS"; } @Override public String getNumericFunctions() { - return "ABS,ACOS,ASIN,ATAN,ATAN2,CBRT,CEIL,CEILING,COS,COT,DEGREES,EXP,FLOOR,LOG,MOD,PI,POW,,POWER,RADIANS,RANDOM,ROUND,SIGN,SIN,SQRT,TAN,TRUNC"; + return "ABS,ACOS,ASIN,ATAN,ATAN2,CBRT,CEIL,COS,COT,DEGREES,EXP,FLOOR,LOG,MOD,PI,POW,RADIANS,RANDOM,ROUND,SIGN,SIN,SQRT,TAN,TRUNC"; } @Override public String getStringFunctions() { - return "BASE64_ENCODE,CONCAT,EXTRACT_ALL,GEN_RANDOM_UUID,ILIKE,LENGTH,LIKE,LOWER,LPAD,LTRIM,MATCH,MATCH_ANY," - + "MD5,MD5_NUMBER_LOWER64,MD5_NUMBER_UPPER64,REGEXP_LIKE,REGEXP_MATCHES,REGEXP_REPLACE,REPEAT,REPLACE,REVERSE," - + "RPAD,RTRIM,SPLIT,SPLIT_PART,STRPOS,SUBSTRING,TO_DATE,TO_DOUBLE,TO_FLOAT,TO_INT,TO_TIMESTAMP,TO_TIMESTAMPTZ,TRIM,UPPER"; + return "ARRAY_ENUMERATE,BASE64_ENCODE,BTRIM,CONCAT,EXTRACT_ALL,GEN_RANDOM_UUID,ILIKE,LENGTH,LIKE,LOWER,LPAD,LTRIM,MATCH,MATCH_ANY," + + "MD5,MD5_NUMBER_LOWER64,MD5_NUMBER_UPPER64,OCTET_LENGTH,REGEXP_LIKE,REGEXP_MATCHES,REGEXP_REPLACE,REPEAT,REPLACE,REVERSE," + + "RPAD,RTRIM,SPLIT,SPLIT_PART,STRPOS,SUBSTRING,SUBSTR,TO_DOUBLE,TO_FLOAT,TO_INT,TRIM,UPPER,URL_DECODE,URL_ENCODE"; } @Override public String getSystemFunctions() { - return "IFNULL"; + return "VERSION"; } @Override public String getTimeDateFunctions() { - return "CURRENT_DATE,CURRENT_TIMESTAMP,DATE_ADD,DATE_DIFF,DATE_TRUNC,EXTRACT,LOCALTIMESTAMP,TO_CHAR,TO_DATE,TO_TIMESTAMP,TO_TIMESTAMPTZ"; + return "CURRENT_DATE,CURRENT_TIMESTAMP,DATE_ADD,DATE_DIFF,DATE_TRUNC,EXTRACT,TO_CHAR,TO_DATE,TO_TIMESTAMP,TO_TIMESTAMPTZ"; } @Override - @ExcludeFromJacocoGeneratedReport - public String getSearchStringEscape() throws SQLException { + public String getSearchStringEscape() { return "\\"; } /** * Returns empty string for compatibility with PostgreSQL. * @return empty string - * @throws SQLException - if fact does not throw exception because the implementation is trivial */ @Override - @ExcludeFromJacocoGeneratedReport - public String getExtraNameCharacters() throws SQLException { + public String getExtraNameCharacters() { return ""; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsAlterTableWithAddColumn() throws SQLException { + public boolean supportsAlterTableWithAddColumn() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsAlterTableWithDropColumn() throws SQLException { + public boolean supportsAlterTableWithDropColumn() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsColumnAliasing() throws SQLException { + public boolean supportsColumnAliasing() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean nullPlusNonNullIsNull() throws SQLException { + public boolean nullPlusNonNullIsNull() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsConvert() throws SQLException { + public boolean supportsConvert() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsConvert(int fromType, int toType) throws SQLException { + public boolean supportsConvert(int fromType, int toType) { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsTableCorrelationNames() throws SQLException { + public boolean supportsTableCorrelationNames() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsDifferentTableCorrelationNames() throws SQLException { + public boolean supportsDifferentTableCorrelationNames() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsExpressionsInOrderBy() throws SQLException { + public boolean supportsExpressionsInOrderBy() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOrderByUnrelated() throws SQLException { + public boolean supportsOrderByUnrelated() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsGroupBy() throws SQLException { + public boolean supportsGroupBy() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsGroupByUnrelated() throws SQLException { - return true; + public boolean supportsGroupByUnrelated() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsGroupByBeyondSelect() throws SQLException { - return true; + public boolean supportsGroupByBeyondSelect() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsLikeEscapeClause() throws SQLException { + public boolean supportsLikeEscapeClause() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMultipleResultSets() throws SQLException { + public boolean supportsMultipleResultSets() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMultipleTransactions() throws SQLException { + public boolean supportsMultipleTransactions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsNonNullableColumns() throws SQLException { + public boolean supportsNonNullableColumns() { return true; } - @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMinimumSQLGrammar() throws SQLException { - return false; + /** + * {@inheritDoc} + *

This grammar is defined at: + * SQL Minimum Grammar + * @return true + * @throws SQLException - actually never throws + */ + @Override + public boolean supportsMinimumSQLGrammar() { + return true; } + /** + * Does this driver support the Core ODBC SQL grammar. We need SQL-92 conformance for this. + * + * @return false + */ @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCoreSQLGrammar() throws SQLException { + public boolean supportsCoreSQLGrammar() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsExtendedSQLGrammar() throws SQLException { + public boolean supportsExtendedSQLGrammar() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsANSI92EntryLevelSQL() throws SQLException { - // We do not support it (eg: we would need to be compliant with JDBC and support - // 'schema') + public boolean supportsANSI92EntryLevelSQL() { + // We do not support it (eg: we would need to be compliant with JDBC and support 'schema') return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsANSI92IntermediateSQL() throws SQLException { + public boolean supportsANSI92IntermediateSQL() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsANSI92FullSQL() throws SQLException { + public boolean supportsANSI92FullSQL() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsIntegrityEnhancementFacility() throws SQLException { + public boolean supportsIntegrityEnhancementFacility() { // Similar approach as pgjdbc: we assume it means supported constraints - return true; + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOuterJoins() throws SQLException { + public boolean supportsOuterJoins() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsFullOuterJoins() throws SQLException { + public boolean supportsFullOuterJoins() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsLimitedOuterJoins() throws SQLException { + public boolean supportsLimitedOuterJoins() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public String getSchemaTerm() throws SQLException { + public String getSchemaTerm() { return "schema"; } @Override - @ExcludeFromJacocoGeneratedReport - public String getProcedureTerm() throws SQLException { + public String getProcedureTerm() { return "procedure"; } @Override - @ExcludeFromJacocoGeneratedReport - public String getCatalogTerm() throws SQLException { + public String getCatalogTerm() { return "database"; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean isCatalogAtStart() throws SQLException { + public boolean isCatalogAtStart() { // it is currently not supported but it will be soon return false; } @Override - @ExcludeFromJacocoGeneratedReport - public String getCatalogSeparator() throws SQLException { + public String getCatalogSeparator() { return "."; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSchemasInDataManipulation() throws SQLException { - return true; + public boolean supportsSchemasInDataManipulation() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSchemasInProcedureCalls() throws SQLException { - return true; + public boolean supportsSchemasInProcedureCalls() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSchemasInTableDefinitions() throws SQLException { - return true; + public boolean supportsSchemasInTableDefinitions() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSchemasInIndexDefinitions() throws SQLException { - return true; + public boolean supportsSchemasInIndexDefinitions() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSchemasInPrivilegeDefinitions() throws SQLException { - return true; + public boolean supportsSchemasInPrivilegeDefinitions() { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCatalogsInDataManipulation() throws SQLException { + public boolean supportsCatalogsInDataManipulation() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCatalogsInProcedureCalls() throws SQLException { + public boolean supportsCatalogsInProcedureCalls() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCatalogsInTableDefinitions() throws SQLException { + public boolean supportsCatalogsInTableDefinitions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCatalogsInIndexDefinitions() throws SQLException { + public boolean supportsCatalogsInIndexDefinitions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCatalogsInPrivilegeDefinitions() throws SQLException { + public boolean supportsCatalogsInPrivilegeDefinitions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsPositionedDelete() throws SQLException { + public boolean supportsPositionedDelete() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsPositionedUpdate() throws SQLException { + public boolean supportsPositionedUpdate() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSelectForUpdate() throws SQLException { + public boolean supportsSelectForUpdate() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsStoredProcedures() throws SQLException { + public boolean supportsStoredProcedures() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSubqueriesInComparisons() throws SQLException { + public boolean supportsSubqueriesInComparisons() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSubqueriesInExists() throws SQLException { - return false; + public boolean supportsSubqueriesInExists() { + return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSubqueriesInIns() throws SQLException { + public boolean supportsSubqueriesInIns() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSubqueriesInQuantifieds() throws SQLException { + public boolean supportsSubqueriesInQuantifieds() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsCorrelatedSubqueries() throws SQLException { + public boolean supportsCorrelatedSubqueries() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsUnion() throws SQLException { + public boolean supportsUnion() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsUnionAll() throws SQLException { + public boolean supportsUnionAll() { return true; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOpenCursorsAcrossCommit() throws SQLException { + public boolean supportsOpenCursorsAcrossCommit() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOpenCursorsAcrossRollback() throws SQLException { + public boolean supportsOpenCursorsAcrossRollback() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOpenStatementsAcrossCommit() throws SQLException { + public boolean supportsOpenStatementsAcrossCommit() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsOpenStatementsAcrossRollback() throws SQLException { + public boolean supportsOpenStatementsAcrossRollback() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxBinaryLiteralLength() throws SQLException { - return 0; + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxBinaryLiteralLength() { + return MAX_LITERAL_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxCharLiteralLength() throws SQLException { - return 0; + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxCharLiteralLength() { + return MAX_LITERAL_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnNameLength() throws SQLException { + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxColumnNameLength() { return MAX_IDENTIFIER_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnsInGroupBy() throws SQLException { - return 0; + public int getMaxColumnsInGroupBy() { + return 0x10000; //65536 } - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnsInIndex() throws SQLException { + /** + * Indexes are not supported, so the value is irrelevant. + * @return 0 + */ + public int getMaxColumnsInIndex() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnsInOrderBy() throws SQLException { - return 0; + public int getMaxColumnsInOrderBy() { + return 16384; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnsInSelect() throws SQLException { - return 0; + public int getMaxColumnsInSelect() { + return 8192; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxColumnsInTable() throws SQLException { + public int getMaxColumnsInTable() { return 1000; } @Override - @ExcludeFromJacocoGeneratedReport - public int getMaxConnections() throws SQLException { + public int getMaxConnections() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxCursorNameLength() throws SQLException { + public int getMaxCursorNameLength() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxIndexLength() throws SQLException { + public int getMaxIndexLength() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxSchemaNameLength() throws SQLException { + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxSchemaNameLength() { return MAX_IDENTIFIER_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxProcedureNameLength() throws SQLException { + public int getMaxProcedureNameLength() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxCatalogNameLength() throws SQLException { - return 0; + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxCatalogNameLength() { + return MAX_IDENTIFIER_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxRowSize() throws SQLException { + public int getMaxRowSize() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean doesMaxRowSizeIncludeBlobs() throws SQLException { - return false; + public boolean doesMaxRowSizeIncludeBlobs() { + return true; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxStatementLength() throws SQLException { + public int getMaxStatementLength() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxStatements() throws SQLException { + public int getMaxStatements() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxTableNameLength() throws SQLException { + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxTableNameLength() { return MAX_IDENTIFIER_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxTablesInSelect() throws SQLException { + public int getMaxTablesInSelect() { return 0; } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public int getMaxUserNameLength() throws SQLException { - return 0; + @SuppressWarnings("java:S4144") // identical implementation + public int getMaxUserNameLength() { + return MAX_IDENTIFIER_LENGTH; } @Override - @ExcludeFromJacocoGeneratedReport - public int getDefaultTransactionIsolation() throws SQLException { + public int getDefaultTransactionIsolation() { return Connection.TRANSACTION_NONE; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsTransactions() throws SQLException { + public boolean supportsTransactions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsDataDefinitionAndDataManipulationTransactions() throws SQLException { + public boolean supportsDataDefinitionAndDataManipulationTransactions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsDataManipulationTransactionsOnly() throws SQLException { + public boolean supportsDataManipulationTransactionsOnly() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean dataDefinitionCausesTransactionCommit() throws SQLException { + public boolean dataDefinitionCausesTransactionCommit() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean dataDefinitionIgnoredInTransactions() throws SQLException { + public boolean dataDefinitionIgnoredInTransactions() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsResultSetType(int type) throws SQLException { - return ResultSet.TYPE_FORWARD_ONLY == type; + public boolean supportsResultSetType(int type) { + return TYPE_FORWARD_ONLY == type; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQLException { - return false; + public boolean supportsResultSetConcurrency(int type, int concurrency) { + return type == ResultSet.TYPE_FORWARD_ONLY && concurrency == ResultSet.CONCUR_READ_ONLY; } /* @@ -1044,269 +1037,384 @@ public boolean supportsResultSetConcurrency(int type, int concurrency) throws SQ * since we do not support updating ResultSet objects */ @Override - @ExcludeFromJacocoGeneratedReport - public boolean ownUpdatesAreVisible(int type) throws SQLException { - return true; + public boolean ownUpdatesAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean ownDeletesAreVisible(int type) throws SQLException { - return true; + public boolean ownDeletesAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean ownInsertsAreVisible(int type) throws SQLException { - return true; + public boolean ownInsertsAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean othersUpdatesAreVisible(int type) throws SQLException { - return true; + public boolean othersUpdatesAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean othersDeletesAreVisible(int type) throws SQLException { - return true; + public boolean othersDeletesAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean othersInsertsAreVisible(int type) throws SQLException { - return true; + public boolean othersInsertsAreVisible(int type) { + return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean updatesAreDetected(int type) throws SQLException { + public boolean updatesAreDetected(int type) { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean deletesAreDetected(int type) throws SQLException { + public boolean deletesAreDetected(int type) { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean insertsAreDetected(int type) throws SQLException { + public boolean insertsAreDetected(int type) { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsBatchUpdates() throws SQLException { + public boolean supportsBatchUpdates() { // We support it partially (via FireboltPreparedStatement but not with the // 'basic' FireboltStatement ) return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsSavepoints() throws SQLException { + public boolean supportsSavepoints() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsNamedParameters() throws SQLException { + public boolean supportsNamedParameters() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsMultipleOpenResults() throws SQLException { + public boolean supportsMultipleOpenResults() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsGetGeneratedKeys() throws SQLException { + public boolean supportsGetGeneratedKeys() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsResultSetHoldability(int holdability) throws SQLException { - return false; + public boolean supportsResultSetHoldability(int holdability) { + return holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT; } + /** + * Since Firebolt does not support transactions commit does not affect the existing {@code ResultSet} and therefore + * it behaves as if it is held between transaction. Therefore, it returns {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + * @return {@link ResultSet#HOLD_CURSORS_OVER_COMMIT} + */ @Override - @ExcludeFromJacocoGeneratedReport - public int getResultSetHoldability() throws SQLException { - // N/A applicable as we do not support transactions - return 0; + public int getResultSetHoldability() { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override - @ExcludeFromJacocoGeneratedReport - public int getSQLStateType() throws SQLException { + public int getSQLStateType() { return sqlStateSQL; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean locatorsUpdateCopy() throws SQLException { + public boolean locatorsUpdateCopy() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsStatementPooling() throws SQLException { + public boolean supportsStatementPooling() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public RowIdLifetime getRowIdLifetime() throws SQLException { + public RowIdLifetime getRowIdLifetime() { return RowIdLifetime.ROWID_UNSUPPORTED; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean supportsStoredFunctionsUsingCallSyntax() throws SQLException { + public boolean supportsStoredFunctionsUsingCallSyntax() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public boolean autoCommitFailureClosesAllResultSets() throws SQLException { + public boolean autoCommitFailureClosesAllResultSets() { return false; } @Override - @ExcludeFromJacocoGeneratedReport - public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, - String columnNamePattern) throws SQLException { - return FireboltResultSet.empty(); - } - - @Override - @ExcludeFromJacocoGeneratedReport - public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) - throws SQLException { - return FireboltResultSet.empty(); + public ResultSet getProcedureColumns(String catalog, String schemaPattern, String procedureNamePattern, String columnNamePattern) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(PROCEDURE_CAT, TEXT), + entry(PROCEDURE_SCHEM, TEXT), + entry(PROCEDURE_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(COLUMN_TYPE, INTEGER), // Short + entry(DATA_TYPE, INTEGER), + entry(TYPE_NAME, TEXT), + entry(PRECISION, INTEGER), + entry(LENGTH, INTEGER), + entry(SCALE, INTEGER), // short + entry(RADIX, INTEGER), // short + entry(NULLABLE, INTEGER), // short + entry(REMARKS, TEXT), + entry(COLUMN_DEF, TEXT), + entry(SQL_DATA_TYPE, INTEGER), + entry(SQL_DATETIME_SUB, INTEGER), + entry(CHAR_OCTET_LENGTH, INTEGER), + entry(ORDINAL_POSITION, INTEGER), + entry(IS_NULLABLE, TEXT), + entry(SPECIFIC_NAME, TEXT) + )); + } + + @Override + public ResultSet getUDTs(String catalog, String schemaPattern, String typeNamePattern, int[] types) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(TYPE_CAT, TEXT), + entry(TYPE_SCHEM, TEXT), + entry(TYPE_NAME, TEXT), + entry(CLASS_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(REMARKS, TEXT), + entry(BASE_TYPE, INTEGER) // short + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getSuperTypes(String catalog, String schemaPattern, String typeNamePattern) throws SQLException { - return FireboltResultSet.empty(); + return createEmptyResultSet(Stream.of( + entry(TYPE_CAT, TEXT), + entry(TYPE_SCHEM, TEXT), + entry(TYPE_NAME, TEXT), + entry(SUPERTYPE_CAT, TEXT), + entry(SUPERTYPE_SCHEM, TEXT), + entry(SUPERTYPE_NAME, TEXT) + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getSuperTables(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { - return FireboltResultSet.empty(); - } - - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, - String attributeNamePattern) throws SQLException { - return FireboltResultSet.empty(); - } - - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) - throws SQLException { - return FireboltResultSet.empty(); - } - - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getColumnPrivileges(String catalog, String schema, String table, String columnNamePattern) - throws SQLException { - return FireboltResultSet.empty(); + return createEmptyResultSet(Stream.of( + entry(TYPE_CAT, TEXT), + entry(TYPE_SCHEM, TEXT), + entry(TYPE_NAME, TEXT), + entry(SUPERTABLE_NAME, TEXT) + )); + } + + @Override + public ResultSet getAttributes(String catalog, String schemaPattern, String typeNamePattern, String attributeNamePattern) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(TYPE_CAT, TEXT), + entry(TYPE_SCHEM, TEXT), + entry(TYPE_NAME, TEXT), + entry(ATTR_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(ATTR_TYPE_NAME, TEXT), + entry(ATTR_SIZE, INTEGER), + entry(DECIMAL_DIGITS, INTEGER), + entry(NUM_PREC_RADIX, INTEGER), + entry(NULLABLE, INTEGER), + entry(REMARKS, TEXT), + entry(ATTR_DEF, TEXT), + entry(SQL_DATA_TYPE, INTEGER), + entry(SQL_DATETIME_SUB, INTEGER), + entry(CHAR_OCTET_LENGTH, INTEGER), + entry(ORDINAL_POSITION, INTEGER), + entry(IS_NULLABLE, TEXT), + entry(SCOPE_CATALOG, TEXT), + entry(SCOPE_SCHEMA, TEXT), + entry(SCOPE_TABLE, TEXT), + entry(SOURCE_DATA_TYPE, INTEGER) // short + )); + } + + @Override + public ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(PROCEDURE_CAT, TEXT), + entry(PROCEDURE_SCHEM, TEXT), + entry(PROCEDURE_NAME, TEXT), + entry(REMARKS, TEXT), + entry(PROCEDURE_TYPE, INTEGER), // short + entry(SPECIFIC_NAME, TEXT) + )); + } + + @Override + public ResultSet getColumnPrivileges(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + List> rows = new ArrayList<>(); + String query = MetadataUtil.getColumnsQuery(schemaPattern, tableNamePattern, columnNamePattern); + try (Statement statement = connection.createStatement(); + ResultSet columnDescription = statement.executeQuery(query)) { + while (columnDescription.next()) { + rows.add(Arrays.asList(connection.getCatalog(), + columnDescription.getString("table_schema"), + columnDescription.getString("table_name"), + columnDescription.getString("column_name"), + null, // grantor + null, // grantee + null, // privilege + "NO")); // is_grantable + } + } + return createResultSet(Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(GRANTOR, TEXT), + entry(GRANTEE, TEXT), + entry(PRIVILEGE, TEXT), + entry(IS_GRANTABLE, TEXT) + ), rows); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) - throws SQLException { - return FireboltResultSet.empty(); + public ResultSet getTablePrivileges(String catalog, String schemaPattern, String tableNamePattern) throws SQLException { + List> rows = new ArrayList<>(); + String query = MetadataUtil.getTablesQuery(catalog, schemaPattern, tableNamePattern, new String[] {"BASE TABLE", "DIMENSION", "FACT"}); + try (Statement statement = connection.createStatement(); + ResultSet columnDescription = statement.executeQuery(query)) { + while (columnDescription.next()) { + rows.add(Arrays.asList(connection.getCatalog(), + columnDescription.getString("table_schema"), + columnDescription.getString("table_name"), + null, // grantor + null, // grantee + null, // privilege + "NO")); // is_grantable + } + } + return createResultSet(Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(GRANTOR, TEXT), + entry(GRANTEE, TEXT), + entry(PRIVILEGE, TEXT), + entry(IS_GRANTABLE, TEXT) + ), rows); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) - throws SQLException { - return FireboltResultSet.empty(); + public ResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope, boolean nullable) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(SCOPE, INTEGER), // short + entry(COLUMN_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(TYPE_NAME, TEXT), + entry(COLUMN_SIZE, INTEGER), + entry(BUFFER_LENGTH, INTEGER), + entry(DECIMAL_DIGITS, INTEGER), // short + entry(PSEUDO_COLUMN, INTEGER) // short + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getVersionColumns(String catalog, String schema, String table) throws SQLException { - return FireboltResultSet.empty(); + return createEmptyResultSet(Stream.of( + entry(SCOPE, INTEGER), // short + entry(COLUMN_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(TYPE_NAME, TEXT), + entry(COLUMN_SIZE, INTEGER), + entry(BUFFER_LENGTH, INTEGER), + entry(DECIMAL_DIGITS, INTEGER), // short + entry(PSEUDO_COLUMN, INTEGER) // short + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getPrimaryKeys(String catalog, String schema, String table) throws SQLException { - return FireboltResultSet.empty(); + return createEmptyResultSet(Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(KEY_SEQ, INTEGER), // short + entry(PK_NAME, TEXT) + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getImportedKeys(String catalog, String schema, String table) throws SQLException { - return FireboltResultSet.empty(); + return getKeys(); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getExportedKeys(String catalog, String schema, String table) throws SQLException { - return FireboltResultSet.empty(); + return getKeys(); } - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable, String foreignCatalog, String foreignSchema, String foreignTable) throws SQLException { - return FireboltResultSet.empty(); + return getKeys(); + } + + private ResultSet getKeys() throws SQLException { + return createEmptyResultSet(Stream.of( + entry(PKTABLE_CAT, TEXT), + entry(PKTABLE_SCHEM, TEXT), + entry(PKTABLE_NAME, TEXT), + entry(PKCOLUMN_NAME, TEXT), + entry(FKTABLE_CAT, TEXT), + entry(FKTABLE_SCHEM, TEXT), + entry(FKTABLE_NAME, TEXT), + entry(FKCOLUMN_NAME, TEXT), + entry(KEY_SEQ, INTEGER), // short + entry(UPDATE_RULE, INTEGER), // short + entry(DELETE_RULE, INTEGER), // short + entry(FK_NAME, TEXT), + entry(PK_NAME, TEXT), + entry(DEFERRABILITY, INTEGER) // short + )); + } + + @Override + public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(NON_UNIQUE, BOOLEAN), + entry(INDEX_QUALIFIER, TEXT), + entry(INDEX_NAME, TEXT), + entry(TYPE, INTEGER), // short + entry(ORDINAL_POSITION, INTEGER), // short + entry(COLUMN_NAME, TEXT), + entry(ASC_OR_DESC, TEXT), + entry(CARDINALITY, BIG_INT), // long + entry(PAGES, BIG_INT), // long + entry(FILTER_CONDITION, INTEGER) + )); } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getIndexInfo(String catalog, String schema, String table, boolean unique, boolean approximate) - throws SQLException { - return FireboltResultSet.empty(); - } - - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public ResultSet getClientInfoProperties() throws SQLException { - return FireboltResultSet.empty(); + return createEmptyResultSet(Stream.of( + entry(NAME, TEXT), + entry(MAX_LEN, INTEGER), + entry(DEFAULT_VALUE, TEXT), + entry(DESCRIPTION, TEXT) + )); } @Override public ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { - List columns = Arrays.asList( - QueryResult.Column.builder().name(FUNCTION_CAT).type(TEXT).build(), - QueryResult.Column.builder().name(FUNCTION_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(FUNCTION_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(REMARKS).type(TEXT).build(), - QueryResult.Column.builder().name(FUNCTION_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(SPECIFIC_NAME).type(TEXT).build()); - Predicate functionFilter = functionNamePattern == null ? f -> true : f -> containsIgnoreCase(f, functionNamePattern); + Predicate functionFilter = functionNamePattern == null ? f -> true : compile(functionNamePattern, CASE_INSENSITIVE).asPredicate(); List> rows = Arrays.stream(String.join(",", getStringFunctions(), getNumericFunctions(), getTimeDateFunctions(), getSystemFunctions()).split(",")) .map(String::trim) // instead of split("\\s*,\\s") blocked by Sonar according to its opinion "can lead denial of service" (?!) @@ -1315,32 +1423,18 @@ public ResultSet getFunctions(String catalog, String schemaPattern, String funct .distinct() // some functions belong to different categories, e.g. TO_DATE is both date-time and string function .map(function -> Arrays.asList(null, null, function, null, functionNoTable, function)) .collect(toList()); - return FireboltResultSet.of(QueryResult.builder().columns(columns).rows(rows).build()); + return createResultSet(Stream.of( + entry(FUNCTION_CAT, TEXT), + entry(FUNCTION_SCHEM, TEXT), + entry(FUNCTION_NAME, TEXT), + entry(REMARKS, TEXT), + entry(FUNCTION_TYPE, INTEGER), + entry(SPECIFIC_NAME, TEXT)), rows); } @Override public ResultSet getFunctionColumns(String catalog, String schemaPattern, String functionNamePattern, String columnNamePattern) throws SQLException { - - List columns = Arrays.asList( - QueryResult.Column.builder().name(FUNCTION_CAT).type(TEXT).build(), - QueryResult.Column.builder().name(FUNCTION_SCHEM).type(TEXT).build(), - QueryResult.Column.builder().name(FUNCTION_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(COLUMN_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(COLUMN_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(DATA_TYPE).type(INTEGER).build(), - QueryResult.Column.builder().name(TYPE_NAME).type(TEXT).build(), - QueryResult.Column.builder().name(PRECISION).type(INTEGER).build(), - QueryResult.Column.builder().name(LENGTH).type(INTEGER).build(), - QueryResult.Column.builder().name(SCALE).type(INTEGER).build(), - QueryResult.Column.builder().name(RADIX).type(INTEGER).build(), - QueryResult.Column.builder().name(NULLABLE).type(INTEGER).build(), - QueryResult.Column.builder().name(REMARKS).type(TEXT).build(), - QueryResult.Column.builder().name(CHAR_OCTET_LENGTH).type(INTEGER).build(), - QueryResult.Column.builder().name(ORDINAL_POSITION).type(INTEGER).build(), - QueryResult.Column.builder().name(IS_NULLABLE).type(TEXT).build(), - QueryResult.Column.builder().name(SPECIFIC_NAME).type(TEXT).build() - ); - Predicate functionFilter = functionNamePattern == null ? f -> true : f -> containsIgnoreCase(f, functionNamePattern); + Predicate functionFilter = functionNamePattern == null ? f -> true : compile(functionNamePattern, CASE_INSENSITIVE).asPredicate(); List> stringFunctions = Arrays.stream(String.join(",", getStringFunctions()).split(",")).map(String::trim).filter(functionFilter) .map(function -> Arrays.asList(null, null, function, null, functionColumnUnknown, Types.VARCHAR, JDBCType.VARCHAR.getName(), null, null, null, null, functionNullable, null, null, null, "YES", function)) @@ -1375,18 +1469,56 @@ public ResultSet getFunctionColumns(String catalog, String schemaPattern, String }; List> allFunctions = Stream.of(stringFunctions, numericFunctions, timeDateFunctions, systemFunctions).flatMap(Collection::stream).sorted(comparator).collect(toList()); - return FireboltResultSet.of(QueryResult.builder().columns(columns).rows(allFunctions).build()); + return createResultSet(Stream.of( + entry(FUNCTION_CAT, TEXT), + entry(FUNCTION_SCHEM, TEXT), + entry(FUNCTION_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(COLUMN_TYPE, INTEGER), + entry(DATA_TYPE, INTEGER), + entry(TYPE_NAME, TEXT), + entry(PRECISION, INTEGER), + entry(LENGTH, INTEGER), + entry(SCALE, INTEGER), + entry(RADIX, INTEGER), + entry(NULLABLE, INTEGER), + entry(REMARKS, TEXT), + entry(CHAR_OCTET_LENGTH, INTEGER), + entry(ORDINAL_POSITION, INTEGER), + entry(IS_NULLABLE, TEXT), + entry(SPECIFIC_NAME, TEXT)), allFunctions); + } + @Override + public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return createEmptyResultSet(Stream.of( + entry(TABLE_CAT, TEXT), + entry(TABLE_SCHEM, TEXT), + entry(TABLE_NAME, TEXT), + entry(COLUMN_NAME, TEXT), + entry(DATA_TYPE, INTEGER), + entry(COLUMN_SIZE, INTEGER), + entry(DECIMAL_DIGITS, INTEGER), + entry(NUM_PREC_RADIX, INTEGER), + entry(COLUMN_USAGE, TEXT), + entry(REMARKS, TEXT), + entry(CHAR_OCTET_LENGTH, INTEGER), + entry(IS_NULLABLE, TEXT) + )); + } + + @Override + public boolean generatedKeyAlwaysReturned() { + return false; } - @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented - public ResultSet getPseudoColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) { - return FireboltResultSet.empty(); + + private ResultSet createEmptyResultSet(Stream> columns) throws SQLException { + return createResultSet(columns, List.of()); } - @Override - @ExcludeFromJacocoGeneratedReport - public boolean generatedKeyAlwaysReturned() throws SQLException { - return false; + private ResultSet createResultSet(Stream> columns, List> rows) throws SQLException { + return FireboltResultSet.of(QueryResult.builder() + .columns(columns.map(e -> QueryResult.Column.builder().name(e.getKey()).type(e.getValue()).build()).collect(toList())) + .rows(rows) + .build()); } } diff --git a/src/main/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadata.java b/src/main/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadata.java index 7f7e39b5..4e339e92 100644 --- a/src/main/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadata.java +++ b/src/main/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadata.java @@ -1,8 +1,6 @@ package com.firebolt.jdbc.metadata; import com.firebolt.jdbc.connection.FireboltConnection; -import com.firebolt.jdbc.resultset.FireboltResultSet; -import org.apache.commons.lang3.StringUtils; import java.sql.ResultSet; import java.sql.SQLException; @@ -11,49 +9,48 @@ * This class represents the database metadata for a system engine */ public class FireboltSystemEngineDatabaseMetadata extends FireboltDatabaseMetadata { + private static final String INFORMATION_SCHEMA = "information_schema"; // the only schema available for system engine public FireboltSystemEngineDatabaseMetadata(String url, FireboltConnection connection) { super(url, connection); } @Override - public ResultSet getSchemas() { - return FireboltResultSet.empty(); + public ResultSet getSchemas() throws SQLException { + return getSchemas(null, INFORMATION_SCHEMA); } @Override - public ResultSet getSchemas(String catalog, String schemaPattern) { - return FireboltResultSet.empty(); + public ResultSet getSchemas(String catalog, String schemaPattern) throws SQLException { + return super.getSchemas(catalog, fixSchema(schemaPattern)); } @Override - public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, - String columnNamePattern) { - return FireboltResultSet.empty(); + public ResultSet getColumns(String catalog, String schemaPattern, String tableNamePattern, String columnNamePattern) throws SQLException { + return super.getColumns(catalog, fixSchema(schemaPattern), tableNamePattern, columnNamePattern); } @Override - public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr) { - return FireboltResultSet.empty(); + public ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String[] typesArr) throws SQLException { + return super.getTables(catalog, fixSchema(schemaPattern), tableNamePattern, typesArr); } - @Override - public int getDatabaseMajorVersion() throws SQLException { - return 0; - } - - @Override - public int getDatabaseMinorVersion() throws SQLException { - return 0; - } - - @Override - public String getDatabaseProductVersion() throws SQLException { - return StringUtils.EMPTY; - } - - @Override - public boolean isReadOnly() throws SQLException { - return true; + /** + * Query {@code select * from information_schema.tables} returns all tables regardless the used engine. + * However, system engine cannot perform query against custom tables. So, according to our understanding + * custom tables should not be returned here because they cannot be considered "available" according to the + * documentation {@link java.sql.DatabaseMetaData#getTables(String, String, String, String[])}. + *
+ * So, this function "fixes" the given {@code schemaPattern}. If it is {@code null} or matches {@code information_schema} + * it remains unchanged, otherwise special not existing value is returned that causes functions that accept {@code schemaPattern} + * to return empty result set. + *
+ * This functionality will be probably changed in future if we allow system engine to perform queries from custom tables. + * + * @param schemaPattern the given schema pattern + * @return "fixed" schema pattern + */ + private String fixSchema(String schemaPattern) { + return schemaPattern == null || INFORMATION_SCHEMA.contains(schemaPattern.replace("%", "").toLowerCase()) ? INFORMATION_SCHEMA : "does_not_exist"; } } diff --git a/src/main/java/com/firebolt/jdbc/metadata/MetadataColumns.java b/src/main/java/com/firebolt/jdbc/metadata/MetadataColumns.java index 69043800..31b3488a 100644 --- a/src/main/java/com/firebolt/jdbc/metadata/MetadataColumns.java +++ b/src/main/java/com/firebolt/jdbc/metadata/MetadataColumns.java @@ -21,6 +21,7 @@ public class MetadataColumns { public static final String NULLABLE = "NULLABLE"; public static final String REMARKS = "REMARKS"; public static final String COLUMN_DEF = "COLUMN_DEF"; + public static final String COLUMN_USAGE = "COLUMN_USAGE"; public static final String CHAR_OCTET_LENGTH = "CHAR_OCTET_LENGTH"; public static final String ORDINAL_POSITION = "ORDINAL_POSITION"; public static final String IS_NULLABLE = "IS_NULLABLE"; @@ -60,4 +61,50 @@ public class MetadataColumns { public static final String LENGTH = "LENGTH"; public static final String SCALE = "SCALE"; public static final String RADIX = "RADIX"; + public static final String KEY_SEQ = "KEY_SEQ"; + public static final String PSEUDO_COLUMN = "PSEUDO_COLUMN"; + public static final String SCOPE = "SCOPE"; + public static final String PROCEDURE_CAT = "PROCEDURE_CAT"; + public static final String PROCEDURE_SCHEM = "PROCEDURE_SCHEM"; + public static final String PROCEDURE_NAME = "PROCEDURE_NAME"; + public static final String PROCEDURE_TYPE = "PROCEDURE_TYPE"; + public static final String SUPERTABLE_NAME = "TABLE_NAME"; + public static final String SUPERTYPE_NAME = "SUPERTYPE_NAME"; + public static final String SUPERTYPE_CAT = "SUPERTYPE_CAT"; + public static final String SUPERTYPE_SCHEM = "SUPERTYPE_SCHEM"; + public static final String CLASS_NAME = "CLASS_NAME"; + public static final String BASE_TYPE = "BASE_TYPE"; + public static final String ATTR_NAME = "ATTR_NAME"; + public static final String ATTR_TYPE_NAME = "ATTR_TYPE_NAME"; + public static final String ATTR_SIZE = "ATTR_SIZE"; + public static final String ATTR_DEF = "ATTR_DEF"; + public static final String NAME = "NAME"; + public static final String MAX_LEN = "MAX_LEN"; + public static final String DEFAULT_VALUE = "DEFAULT_VALUE"; + public static final String DESCRIPTION = "DESCRIPTION"; + public static final String PKTABLE_CAT = "PKTABLE_CAT"; + public static final String PKTABLE_SCHEM = "PKTABLE_SCHEM"; + public static final String PKTABLE_NAME = "PKTABLE_NAME"; + public static final String PKCOLUMN_NAME = "PKCOLUMN_NAME"; + public static final String FKTABLE_CAT = "FKTABLE_CAT"; + public static final String FKTABLE_SCHEM = "FKTABLE_SCHEM"; + public static final String FKTABLE_NAME = "FKTABLE_NAME"; + public static final String FKCOLUMN_NAME = "FKCOLUMN_NAME"; + public static final String UPDATE_RULE = "UPDATE_RULE"; + public static final String DELETE_RULE = "DELETE_RULE"; + public static final String FK_NAME = "FK_NAME"; + public static final String PK_NAME = "PK_NAME"; + public static final String DEFERRABILITY = "DEFERRABILITY"; + public static final String GRANTOR = "GRANTOR"; + public static final String GRANTEE = "GRANTEE"; + public static final String PRIVILEGE = "PRIVILEGE"; + public static final String IS_GRANTABLE = "IS_GRANTABLE"; + public static final String NON_UNIQUE = "NON_UNIQUE"; + public static final String INDEX_QUALIFIER = "INDEX_QUALIFIER"; + public static final String INDEX_NAME = "INDEX_NAME"; + public static final String TYPE = "TYPE"; + public static final String ASC_OR_DESC = "ASC_OR_DESC"; + public static final String CARDINALITY = "CARDINALITY"; + public static final String PAGES = "PAGES"; + public static final String FILTER_CONDITION = "FILTER_CONDITION"; } diff --git a/src/main/java/com/firebolt/jdbc/metadata/MetadataUtil.java b/src/main/java/com/firebolt/jdbc/metadata/MetadataUtil.java index a56685f7..299c69fe 100644 --- a/src/main/java/com/firebolt/jdbc/metadata/MetadataUtil.java +++ b/src/main/java/com/firebolt/jdbc/metadata/MetadataUtil.java @@ -1,14 +1,17 @@ package com.firebolt.jdbc.metadata; +import lombok.Builder; +import lombok.Value; +import lombok.experimental.UtilityClass; + import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; -import java.util.Optional; - -import com.firebolt.jdbc.Query; -import lombok.NonNull; -import lombok.experimental.UtilityClass; +import static java.lang.String.format; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.joining; @UtilityClass public class MetadataUtil { @@ -38,68 +41,77 @@ public String getColumnsQuery(String schemaPattern, String tableNamePattern, Str .from("information_schema.columns"); List conditions = new ArrayList<>(); - Optional.ofNullable(tableNamePattern) - .ifPresent(pattern -> conditions.add(String.format("table_name LIKE '%s'", pattern))); - Optional.ofNullable(columnNamePattern) - .ifPresent(pattern -> conditions.add(String.format("column_name LIKE '%s'", pattern))); - Optional.ofNullable(schemaPattern) - .ifPresent(pattern -> conditions.add(String.format("table_schema LIKE '%s'", pattern))); + ofNullable(tableNamePattern).ifPresent(pattern -> conditions.add(format("table_name LIKE '%s'", pattern))); + ofNullable(columnNamePattern).ifPresent(pattern -> conditions.add(format("column_name LIKE '%s'", pattern))); + ofNullable(schemaPattern).ifPresent(pattern -> conditions.add(format("table_schema LIKE '%s'", pattern))); return queryBuilder.conditions(conditions).build().toSql(); } - public String getTablesQuery(String catalog, String schema, String tableName) { - Query.QueryBuilder queryBuilder = Query.builder().select("table_schema, table_name, table_type") - .from("information_schema.tables"); - - List conditions = getConditionsForTables(catalog, schema, tableName); - - queryBuilder.orderBy("table_schema, table_name"); - return queryBuilder.conditions(conditions).build().toSql(); - } - - public String getViewsQuery(String catalog, String schemaPattern, String tableNamePattern) { - - Query.QueryBuilder queryBuilder = Query.builder().select("table_schema, table_name") - .from("information_schema.views"); - - List conditions = getConditionsForViews(catalog, schemaPattern, tableNamePattern); - - queryBuilder.orderBy("table_schema, table_name"); - return queryBuilder.conditions(conditions).build().toSql(); - } - - @NonNull - private List getConditionsForTables(String catalog, String schema, String tableName) { - return getConditionsForTablesAndViews(catalog, schema, tableName, true); - } - - @NonNull - private List getConditionsForViews(String catalog, String schema, String tableName) { - return getConditionsForTablesAndViews(catalog, schema, tableName, false); + public String getTablesQuery(@SuppressWarnings("java:S1172") String catalog, String schema, String tableName, String[] types) { + Query.QueryBuilder queryBuilder = Query.builder().select("table_schema, table_name, table_type").from("information_schema.tables"); + List conditions = new ArrayList<>(); + conditions.add(format("table_type IN (%s)", Arrays.stream(types).map(t -> format("'%s'", t)).collect(joining(", ")))); + // Uncomment once table catalogs are supported. Remove suppress warning ava:S1172 from the first parameter also. + //ofNullable(catalog).ifPresent(pattern -> conditions.add(String.format("table_catalog LIKE '%s'",pattern))); + ofNullable(schema).ifPresent(pattern -> conditions.add(format("table_schema LIKE '%s'", pattern))); + ofNullable(tableName).ifPresent(pattern -> conditions.add(format("table_name LIKE '%s'", pattern))); + return queryBuilder.conditions(conditions).orderBy("table_schema, table_name").build().toSql(); } - @NonNull - private List getConditionsForTablesAndViews(String catalog, String schema, String tableName, - boolean isTable) { - List conditions = new ArrayList<>(); - Optional.ofNullable(schema) - .ifPresent(pattern -> conditions.add(String.format("table_schema LIKE '%s'", pattern))); + /** + * Represents a SQL query that can be sent to Firebolt to receive metadata info + */ + @Builder + @Value + public static class Query { + String select; + String from; + String innerJoin; + String orderBy; + List conditions; + + /** + * Parse the object to a SQL query that can be sent to Firebolt + * + * @return SQL query that can be sent to Firebolt + */ + public String toSql() { + StringBuilder query = new StringBuilder(); + if (select == null || select.isBlank()) { + throw new IllegalStateException("Cannot create query: SELECT cannot be blank"); + } + if (from == null || from.isBlank()) { + throw new IllegalStateException("Cannot create query: FROM cannot be blank"); + } + + query.append("SELECT ").append(select); + query.append(" FROM ").append(from); + if (innerJoin != null && !innerJoin.isBlank()) { + query.append(" JOIN ").append(innerJoin); + } + query.append(getConditionsPart()); + if (orderBy != null && !orderBy.isBlank()) { + query.append(" order by ").append(orderBy); + } + return query.toString(); + } - Optional.ofNullable(tableName) - .ifPresent(pattern -> conditions.add(String.format("table_name LIKE '%s'", pattern))); - // Uncomment once table catalogs are supported - // Optional.ofNullable(catalog) - // .ifPresent(pattern -> conditions.add(String.format("table_catalog LIKE '%s'", - // pattern))); - if (isTable) { - conditions.add("table_type NOT LIKE 'EXTERNAL'"); + private String getConditionsPart() { + StringBuilder agg = new StringBuilder(); + Iterator iter = conditions.iterator(); + if (iter.hasNext()) { + agg.append(" WHERE "); + } + if (iter.hasNext()) { + String entry = iter.next(); + agg.append(entry); + } + while (iter.hasNext()) { + String entry = iter.next(); + agg.append(" AND ").append(entry); + } + return agg.toString(); } - return conditions; } - public static String getDatabaseVersionQuery(String engine) { - return Query.builder().select("version").from("information_schema.engines") - .conditions(Collections.singletonList(String.format("engine_name iLIKE '%s%%'", engine))).build() - .toSql(); - } } diff --git a/src/main/java/com/firebolt/jdbc/resultset/FieldTypeConverter.java b/src/main/java/com/firebolt/jdbc/resultset/FieldTypeConverter.java index 9e306742..1ee4537c 100644 --- a/src/main/java/com/firebolt/jdbc/resultset/FieldTypeConverter.java +++ b/src/main/java/com/firebolt/jdbc/resultset/FieldTypeConverter.java @@ -32,15 +32,15 @@ public class FieldTypeConverter { return BaseType.TEXT.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Integer.class, (value, columnType, column) -> { - verify(Integer.class, columnType, BaseType.INTEGER, BaseType.SHORT); + verifyNumeric(Long.class, columnType); return BaseType.INTEGER.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Long.class, (value, columnType, column) -> { - verify(Long.class, columnType, BaseType.INTEGER, BaseType.SHORT, BaseType.LONG); + verifyNumeric(Long.class, columnType); return BaseType.LONG.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Double.class, (value, columnType, column) -> { - verify(Double.class, columnType, BaseType.DOUBLE, BaseType.REAL, BaseType.INTEGER, BaseType.SHORT, BaseType.LONG, BaseType.BIGINT); + verifyNumeric(Long.class, columnType); return BaseType.DOUBLE.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Boolean.class, (value, columnType, column) -> { @@ -48,20 +48,23 @@ public class FieldTypeConverter { return BaseType.BOOLEAN.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Short.class, (value, columnType, column) -> { - verify(Short.class, columnType, BaseType.SHORT); + verifyNumeric(Long.class, columnType); return BaseType.SHORT.transform(value, column); }); + CLASS_TO_CONVERT_FUNCTION.put(Byte.class, (value, columnType, column) -> { + verifyNumeric(Float.class, columnType); + return BaseType.BYTE.transform(value, column); + }); CLASS_TO_CONVERT_FUNCTION.put(BigInteger.class, (value, columnType, column) -> { - verify(BigInteger.class, columnType, BaseType.INTEGER, BaseType.SHORT, BaseType.LONG, BaseType.BIGINT); + verifyNumeric(Long.class, columnType); return BaseType.BIGINT.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Float.class, (value, columnType, column) -> { - verify(Float.class, columnType, BaseType.REAL, BaseType.INTEGER, BaseType.SHORT, BaseType.LONG, BaseType.BIGINT); + verifyNumeric(Float.class, columnType); return BaseType.REAL.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(BigDecimal.class, (value, columnType, column) -> { - verify(BigDecimal.class, columnType, BaseType.BIGINT, BaseType.NUMERIC, BaseType.INTEGER, BaseType.REAL, - BaseType.DOUBLE); + verifyNumeric(Long.class, columnType); return BaseType.NUMERIC.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(Date.class, (value, columnType, column) -> { @@ -81,8 +84,7 @@ public class FieldTypeConverter { return BaseType.ARRAY.transform(value, column); }); CLASS_TO_CONVERT_FUNCTION.put(OffsetDateTime.class, (value, columnType, column) -> { - verify(OffsetDateTime.class, columnType, BaseType.DATE, BaseType.TIMESTAMP, - BaseType.TIMESTAMP_WITH_TIMEZONE); + verify(OffsetDateTime.class, columnType, BaseType.DATE, BaseType.TIMESTAMP, BaseType.TIMESTAMP_WITH_TIMEZONE); Timestamp ts = BaseType.TIMESTAMP.transform(value, column); return SqlDateUtil.transformFromTimestampToOffsetDateTime.apply(ts); }); @@ -95,14 +97,16 @@ public class FieldTypeConverter { verify(byte[].class, columnType, BaseType.BYTEA); return BaseType.BYTEA.transform(value, column); } else { - return Optional.ofNullable(value).map(v -> BaseType.isNull(v) ? null : v).map(String::getBytes) - .orElse(null); + return Optional.ofNullable(value).map(v -> BaseType.isNull(v) ? null : v).map(String::getBytes).orElse(null); } }); } - private static void verify(Class toType, BaseType columnBaseType, BaseType... supportedTypes) - throws FireboltException { + private static void verifyNumeric(Class toType, BaseType columnBaseType) throws SQLException { + verify(toType, columnBaseType, BaseType.REAL, BaseType.DOUBLE, BaseType.BYTE, BaseType.SHORT, BaseType.INTEGER, BaseType.LONG, BaseType.BIGINT, BaseType.NUMERIC); + } + + private static void verify(Class toType, BaseType columnBaseType, BaseType... supportedTypes) throws SQLException { if (Arrays.stream(supportedTypes).noneMatch(b -> b.equals(columnBaseType))) { throw new FireboltException( String.format(CONVERSION_NOT_SUPPORTED_EXCEPTION, toType, columnBaseType.getType().getName())); @@ -114,6 +118,7 @@ static T convert(Class type, String value, BaseType columnType, Column co throw new FireboltException( String.format(CONVERSION_NOT_SUPPORTED_EXCEPTION, type.getName(), columnType.getType().getName())); } + //noinspection unchecked return (T) CLASS_TO_CONVERT_FUNCTION.get(type).apply(value, columnType, column); } } diff --git a/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSet.java b/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSet.java index 72f366b3..af94336d 100644 --- a/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSet.java +++ b/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSet.java @@ -1,27 +1,12 @@ package com.firebolt.jdbc.resultset; -import java.io.*; -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.sql.*; -import java.sql.Date; -import java.util.*; -import java.util.stream.Collectors; -import java.util.stream.IntStream; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.commons.text.StringEscapeUtils; - -import com.firebolt.jdbc.util.LoggerUtil; +import com.firebolt.jdbc.JdbcBase; import com.firebolt.jdbc.QueryResult; import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; import com.firebolt.jdbc.annotation.NotImplemented; +import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException; -import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException; import com.firebolt.jdbc.resultset.column.Column; import com.firebolt.jdbc.resultset.column.ColumnType; import com.firebolt.jdbc.resultset.compress.LZ4InputStream; @@ -29,20 +14,74 @@ import com.firebolt.jdbc.type.BaseType; import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.type.array.FireboltArray; +import com.firebolt.jdbc.type.array.SqlArrayUtil; +import com.firebolt.jdbc.type.lob.FireboltBlob; +import com.firebolt.jdbc.type.lob.FireboltClob; +import com.firebolt.jdbc.util.LoggerUtil; +import org.apache.commons.text.StringEscapeUtils; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.JDBCType; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.TimeZone; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; -import lombok.CustomLog; +import static com.firebolt.jdbc.type.BaseType.isNull; +import static com.firebolt.jdbc.util.StringUtil.splitAll; +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.ofNullable; /** * ResultSet for InputStream using the format "TabSeparatedWithNamesAndTypes" */ -@CustomLog -public class FireboltResultSet implements ResultSet { +public class FireboltResultSet extends JdbcBase implements ResultSet { + private static final String FORWARD_ONLY_ERROR = "Cannot call %s() for ResultSet of type TYPE_FORWARD_ONLY"; + private static final int DEFAULT_CHAR_BUFFER_SIZE = 8192; // the default of BufferedReader + private static final Logger log = Logger.getLogger(FireboltResultSet.class.getName()); private final BufferedReader reader; private final Map columnNameToColumnNumber; private final FireboltResultSetMetaData resultSetMetaData; private final FireboltStatement statement; private final List columns; private final int maxRows; + private final int maxFieldSize; private String currentLine; private int currentRow = 0; private int lastSplitRow = -1; @@ -51,89 +90,51 @@ public class FireboltResultSet implements ResultSet { private String lastReadValue = null; - public FireboltResultSet(InputStream is, String tableName, String dbName) throws SQLException { - this(is, tableName, dbName, null, false, null, false); - } - - public FireboltResultSet(InputStream is) throws SQLException { - this(is, null, null, null, false, null, false); - } - - public FireboltResultSet(InputStream is, String tableName, String dbName, Integer bufferSize) throws SQLException { - this(is, tableName, dbName, bufferSize, false, null, false); - } - - public FireboltResultSet(InputStream is, String tableName, String dbName, Integer bufferSize, - FireboltStatement fireboltStatement) throws SQLException { - this(is, tableName, dbName, bufferSize, false, fireboltStatement, false); - } - - private FireboltResultSet() { - reader = // empty InputStream - new BufferedReader( - new InputStreamReader(new ByteArrayInputStream("".getBytes()), StandardCharsets.UTF_8)); - resultSetMetaData = FireboltResultSetMetaData.builder().columns(new ArrayList<>()).build(); - columnNameToColumnNumber = new HashMap<>(); - currentLine = null; - columns = new ArrayList<>(); - statement = null; - maxRows = 0; - } - - public FireboltResultSet(InputStream is, String tableName, String dbName, Integer bufferSize, boolean isCompressed, + @SuppressWarnings("java:S2139") // TODO: Exceptions should be either logged or rethrown but not both + public FireboltResultSet(InputStream is, String tableName, String dbName, int bufferSize, boolean isCompressed, FireboltStatement statement, boolean logResultSet) throws SQLException { - this(is, tableName, dbName, bufferSize, 0, isCompressed, statement, logResultSet); - } - - @SuppressWarnings("java:S107") //Number of parameters (8) > max (7). This is the price of the immutability - public FireboltResultSet(InputStream is, String tableName, String dbName, Integer bufferSize, int maxRows, boolean isCompressed, - FireboltStatement statement, boolean logResultSet) throws SQLException { - log.debug("Creating resultSet..."); + log.fine("Creating resultSet..."); this.statement = statement; if (logResultSet) { is = LoggerUtil.logInputStream(is); } this.reader = createStreamReader(is, bufferSize, isCompressed); - this.maxRows = maxRows; + if (statement == null) { + this.maxRows = 0; + this.maxFieldSize = 0; + } else { + this.maxRows = statement.getMaxRows(); + this.maxFieldSize = statement.getMaxFieldSize(); + } try { - this.next(); + next(); String[] fields = toStringArray(currentLine); this.columnNameToColumnNumber = getColumnNamesToIndexes(fields); - if (this.next()) { - this.columns = getColumns(fields, currentLine); - } else { - this.columns = new ArrayList<>(); - } - resultSetMetaData = FireboltResultSetMetaData.builder().columns(this.columns).tableName(tableName) - .dbName(dbName).build(); + columns = next() ? getColumns(fields, currentLine) : new ArrayList<>(); + resultSetMetaData = new FireboltResultSetMetaData(dbName, tableName, columns); } catch (Exception e) { - log.error("Could not create ResultSet: " + ExceptionUtils.getStackTrace(e), e); - throw new FireboltException("Cannot read response from DB: error while creating ResultSet ", e); + log.log(Level.SEVERE, e, () -> "Could not create ResultSet: " + e.getMessage()); + throw new FireboltException("Cannot read response from DB: error while creating ResultSet", e); } - log.debug("ResultSet created"); - } - - public static FireboltResultSet empty() { - return new FireboltResultSet(); + log.fine("ResultSet created"); } public static FireboltResultSet of(QueryResult queryResult) throws SQLException { return new FireboltResultSet(new ByteArrayInputStream(queryResult.toString().getBytes()), - queryResult.getTableName(), queryResult.getDatabaseName()); + queryResult.getTableName(), queryResult.getDatabaseName(), DEFAULT_CHAR_BUFFER_SIZE, false, null, false); } - private BufferedReader createStreamReader(InputStream is, Integer bufferSize, boolean isCompressed) { + private BufferedReader createStreamReader(InputStream is, int bufferSize, boolean isCompressed) { InputStreamReader inputStreamReader; if (isCompressed) { - inputStreamReader = new InputStreamReader(new LZ4InputStream(is), StandardCharsets.UTF_8); + inputStreamReader = new InputStreamReader(new LZ4InputStream(is), UTF_8); } else { - inputStreamReader = new InputStreamReader(is, StandardCharsets.UTF_8); + inputStreamReader = new InputStreamReader(is, UTF_8); } - return bufferSize != null && bufferSize != 0 ? new BufferedReader(inputStreamReader, bufferSize) - : new BufferedReader(inputStreamReader); + return new BufferedReader(inputStreamReader, bufferSize); } @Override @@ -158,13 +159,21 @@ public boolean next() throws SQLException { @Override public String getString(int columnIndex) throws SQLException { - Column columnInfo = this.columns.get(columnIndex - 1); - if (Optional.ofNullable(columnInfo).map(Column::getType).map(ColumnType::getDataType) + Column columnInfo = columns.get(columnIndex - 1); + if (ofNullable(columnInfo).map(Column::getType).map(ColumnType::getDataType) .filter(t -> t.equals(FireboltDataType.BYTEA)).isPresent()) { // We do not need to escape when the type is BYTEA - return this.getValueAtColumn(columnIndex); + String hex = getValueAtColumn(columnIndex); + if (isNull(hex)) { + return null; + } + int maxHexStringSize = maxFieldSize * 2 + 2; + if (maxFieldSize > 0 && maxHexStringSize <= hex.length()) { + hex = hex.substring(0, maxHexStringSize); + } + return hex; } else { - return BaseType.TEXT.transform(this.getValueAtColumn(columnIndex)); + return BaseType.TEXT.transform(getValueAtColumn(columnIndex), null, null, maxFieldSize); } } @@ -175,93 +184,93 @@ public String getString(String column) throws SQLException { @Override public int getInt(int columnIndex) throws SQLException { - Integer value = BaseType.INTEGER.transform(getValueAtColumn(columnIndex)); - return value == null ? 0 : value; + return getValue(columnIndex, BaseType.INTEGER, 0); } @Override public int getInt(String columnName) throws SQLException { - return this.getInt(findColumn(columnName)); + return getInt(findColumn(columnName)); } @Override - public long getLong(int colNum) throws SQLException { - Long value = BaseType.LONG.transform(getValueAtColumn(colNum)); - return value == null ? 0 : value; + public long getLong(int columnIndex) throws SQLException { + return getValue(columnIndex, BaseType.LONG, 0L); } @Override public float getFloat(int columnIndex) throws SQLException { - Float value = BaseType.REAL.transform(getValueAtColumn(columnIndex)); - return value == null ? 0 : value; + return getValue(columnIndex, BaseType.REAL, 0.0F); } @Override public float getFloat(String columnLabel) throws SQLException { - return this.getFloat(findColumn(columnLabel)); + return getFloat(findColumn(columnLabel)); } @Override public double getDouble(int columnIndex) throws SQLException { - Double value = BaseType.DOUBLE.transform(getValueAtColumn(columnIndex)); - return value == null ? 0 : value; + return getValue(columnIndex, BaseType.DOUBLE, 0.0); } @Override public double getDouble(String columnLabel) throws SQLException { - return this.getDouble(findColumn(columnLabel)); + return getDouble(findColumn(columnLabel)); } @Override public long getLong(String column) throws SQLException { - return this.getLong(findColumn(column)); + return getLong(findColumn(column)); } @Override public byte getByte(int columnIndex) throws SQLException { - return Optional.ofNullable(getValueAtColumn(columnIndex)).map(v -> BaseType.isNull(v) ? null : v) - .map(Byte::parseByte).orElse((byte) 0); + return getValue(columnIndex, BaseType.BYTE, (byte)0); } @Override public short getShort(int columnIndex) throws SQLException { - Short value = BaseType.SHORT.transform(getValueAtColumn(columnIndex)); - return value == null ? 0 : value; + return getValue(columnIndex, BaseType.SHORT, (short)0); } @Override public byte getByte(String column) throws SQLException { - return this.getByte(findColumn(column)); + return getByte(findColumn(column)); } @Override public short getShort(String columnLabel) throws SQLException { - return this.getShort(findColumn(columnLabel)); + return getShort(findColumn(columnLabel)); + } + + private T getValue(int columnIndex, BaseType type, T defaultValue) throws SQLException { + T value = type.transform(getValueAtColumn(columnIndex)); + return value == null ? defaultValue : value; } @Override public byte[] getBytes(int colNum) throws SQLException { - return Optional.ofNullable(getValueAtColumn(colNum)).map(v -> BaseType.isNull(v) ? null : v) - .map(String::getBytes).orElse(null); + return ofNullable(getValueAtColumn(colNum)) + .map(v -> isNull(v) ? null : v) + .map(SqlArrayUtil::hexStringToByteArray) + .orElse(null); } @Override public byte[] getBytes(String column) throws SQLException { - return this.getBytes(findColumn(column)); + return getBytes(findColumn(column)); } @Override public synchronized void close() throws SQLException { - if (!this.isClosed) { + if (!isClosed) { try { - this.reader.close(); - this.isClosed = true; + reader.close(); + isClosed = true; } catch (IOException e) { throw new SQLException("Could not close data stream when closing ResultSet", e); } finally { - if (this.statement != null - && (this.statement.isCloseOnCompletion() && !this.statement.hasMoreResults())) { - this.statement.close(); + if (statement != null && (statement.isCloseOnCompletion() && !statement.hasMoreResults())) { + statement.close(); } } } @@ -274,54 +283,48 @@ public int getType() { @Override public BigDecimal getBigDecimal(int columnIndex) throws SQLException { - String value = getValueAtColumn(columnIndex); - if (StringUtils.isEmpty(value)) { - return null; - } - return BaseType.NUMERIC.transform(value); + return getValue(columnIndex, BaseType.NUMERIC, null); } @Override public BigDecimal getBigDecimal(String columnLabel) throws SQLException { - return getBigDecimal(this.findColumn(columnLabel)); + return getBigDecimal(findColumn(columnLabel)); } @Override public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { - BigDecimal bigDecimal = this.getBigDecimal(columnIndex); - return bigDecimal == null ? null : bigDecimal.setScale(scale, RoundingMode.HALF_UP); + return Optional.ofNullable(getBigDecimal(columnIndex)).map(d -> d.setScale(scale, RoundingMode.HALF_UP)).orElse(null); } @Override public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { - return getBigDecimal(this.findColumn(columnLabel), scale); + return getBigDecimal(findColumn(columnLabel), scale); } @Override public Array getArray(int columnIndex) throws SQLException { - String value = getValueAtColumn(columnIndex); - return BaseType.ARRAY.transform(value, this.resultSetMetaData.getColumn(columnIndex)); + return BaseType.ARRAY.transform(getValueAtColumn(columnIndex), resultSetMetaData.getColumn(columnIndex)); } @Override public Array getArray(String column) throws SQLException { - return this.getArray(this.findColumn(column)); + return getArray(findColumn(column)); } @Override public boolean getBoolean(String columnLabel) throws SQLException { - return getBoolean(this.findColumn(columnLabel)); + return getBoolean(findColumn(columnLabel)); } @Override public boolean getBoolean(int columnIndex) throws SQLException { - Boolean value = BaseType.BOOLEAN.transform(this.getValueAtColumn(columnIndex), this.resultSetMetaData.getColumn(columnIndex)); + Boolean value = BaseType.BOOLEAN.transform(getValueAtColumn(columnIndex), resultSetMetaData.getColumn(columnIndex)); return value != null && value; } @Override public Date getDate(String columnLabel) throws SQLException { - return getDate(this.findColumn(columnLabel)); + return getDate(findColumn(columnLabel)); } @Override @@ -331,43 +334,43 @@ public Date getDate(int columnIndex) throws SQLException { @Override public Date getDate(int columnIndex, Calendar calendar) throws SQLException { - TimeZone timeZone = calendar != null ? calendar.getTimeZone() : null; - String value = this.getValueAtColumn(columnIndex); - return BaseType.DATE.transform(value, this.resultSetMetaData.getColumn(columnIndex), timeZone); + return getDateTime(columnIndex, calendar, BaseType.DATE); } @Override public Date getDate(String columnLabel, Calendar cal) throws SQLException { - return getDate(this.findColumn(columnLabel), cal); + return getDate(findColumn(columnLabel), cal); } @Override public Timestamp getTimestamp(int columnIndex) throws SQLException { - return this.getTimestamp(columnIndex, null); + return getTimestamp(columnIndex, null); } @Override public Timestamp getTimestamp(String columnLabel) throws SQLException { - return getTimestamp(this.findColumn(columnLabel)); + return getTimestamp(findColumn(columnLabel)); } @Override public Timestamp getTimestamp(int columnIndex, Calendar calendar) throws SQLException { - TimeZone timeZone = calendar != null ? calendar.getTimeZone() : null; - String value = this.getValueAtColumn(columnIndex); - return BaseType.TIMESTAMP.transform(value, this.resultSetMetaData.getColumn(columnIndex), timeZone); + return getDateTime(columnIndex, calendar, BaseType.TIMESTAMP); } @Override public Timestamp getTimestamp(String columnLabel, Calendar calendar) throws SQLException { - return getTimestamp(this.findColumn(columnLabel), calendar); + return getTimestamp(findColumn(columnLabel), calendar); } @Override public Time getTime(int columnIndex, Calendar calendar) throws SQLException { + return getDateTime(columnIndex, calendar, BaseType.TIME); + } + + private T getDateTime(int columnIndex, Calendar calendar, BaseType type) throws SQLException { TimeZone timeZone = calendar != null ? calendar.getTimeZone() : null; - String value = this.getValueAtColumn(columnIndex); - return BaseType.TIME.transform(value, this.resultSetMetaData.getColumn(columnIndex), timeZone); + String value = getValueAtColumn(columnIndex); + return type.transform(value, resultSetMetaData.getColumn(columnIndex), timeZone, 0); } @Override @@ -375,35 +378,35 @@ public T getObject(int columnIndex, Class type) throws SQLException { if (type == null) { throw new FireboltException("The type provided is null"); } - String value = this.getValueAtColumn(columnIndex); - Column column = this.resultSetMetaData.getColumn(columnIndex); + String value = getValueAtColumn(columnIndex); + Column column = resultSetMetaData.getColumn(columnIndex); BaseType columnType = column.getType().getDataType().getBaseType(); return FieldTypeConverter.convert(type, value, columnType, column); } @Override public T getObject(String columnLabel, Class type) throws SQLException { - return getObject(this.findColumn(columnLabel), type); + return getObject(findColumn(columnLabel), type); } @Override public Time getTime(String columnLabel) throws SQLException { - return getTime(this.findColumn(columnLabel)); + return getTime(findColumn(columnLabel)); } @Override public Time getTime(int columnIndex) throws SQLException { - return this.getTime(columnIndex, null); + return getTime(columnIndex, null); } @Override public Time getTime(String columnLabel, Calendar cal) throws SQLException { - return getTime(this.findColumn(columnLabel), cal); + return getTime(findColumn(columnLabel), cal); } @Override public int getRow() { - return currentRow; + return currentRow - 2; } @Override @@ -413,18 +416,18 @@ public boolean isClosed() { @Override public ResultSetMetaData getMetaData() throws SQLException { - return this.resultSetMetaData; + return resultSetMetaData; } @Override public Object getObject(int columnIndex) throws SQLException { String value = getValueAtColumn(columnIndex); - if (BaseType.isNull(value)) { + if (isNull(value)) { return null; } - Column columnInfo = this.columns.get(columnIndex - 1); + Column columnInfo = columns.get(columnIndex - 1); FireboltDataType columnType = columnInfo.getType().getDataType(); - Object object = columnType.getBaseType().transform(value, columnInfo, columnInfo.getType().getTimeZone()); + Object object = columnType.getBaseType().transform(value, columnInfo, columnInfo.getType().getTimeZone(), maxFieldSize); if (columnType == FireboltDataType.ARRAY && object != null) { return ((FireboltArray) object).getArray(); } else { @@ -444,10 +447,11 @@ public boolean isBeforeFirst() throws SQLException { } @Override - public boolean isAfterLast() throws SQLException { + public boolean isAfterLast() { return !hasNext() && currentLine == null; } + @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean hasNext() { return reader.lines().iterator().hasNext(); } @@ -459,7 +463,7 @@ public boolean isFirst() throws SQLException { } @Override - public boolean isLast() throws SQLException { + public boolean isLast() { return !hasNext() && currentLine != null; } @@ -469,28 +473,22 @@ public boolean wasNull() throws SQLException { if (lastReadValue == null) { throw new IllegalArgumentException("A column must be read before checking nullability"); } - return BaseType.isNull(lastReadValue); + return isNull(lastReadValue); } @Override public boolean first() throws SQLException { - throw new FireboltException("Cannot call first() for ResultSet of type TYPE_FORWARD_ONLY"); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "first")); } @Override public boolean last() throws SQLException { - throw new FireboltException("Cannot call last() for ResultSet of type TYPE_FORWARD_ONLY"); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "last")); } private String[] toStringArray(String stringToSplit) { if (currentRow != lastSplitRow) { - if (StringUtils.isNotEmpty(stringToSplit)) { - arr = StringUtils.splitPreserveAllTokens(stringToSplit, '\t'); - } else if (StringUtils.equals(stringToSplit, "")) { - arr = new String[] { "" }; - } else { - arr = new String[0]; - } + arr = splitAll(stringToSplit, '\t'); lastSplitRow = currentRow; } return arr; @@ -516,7 +514,7 @@ private int getColumnIndex(int colNum) throws SQLException { } private Map getColumnNamesToIndexes(String[] fields) { - Map columnNameToFieldIndex = new HashMap<>(); + Map columnNameToFieldIndex = new TreeMap<>(CASE_INSENSITIVE_ORDER); if (fields != null) { for (int i = 0; i < fields.length; i++) { columnNameToFieldIndex.put(fields[i], i + 1); @@ -534,7 +532,7 @@ private void checkStreamNotClosed() throws SQLException { private void validateColumnNumber(int columnNumber) throws SQLException { if (columnNumber > columns.size()) { throw new SQLException( - String.format("There is no column with number %d. Total of of columns available: %d ", columnNumber, + format("There is no column with number %d. Total of of columns available: %d ", columnNumber, columns.size())); } } @@ -543,153 +541,103 @@ private void validateColumnNumber(int columnNumber) throws SQLException { public int findColumn(String columnName) throws SQLException { Integer index = columnNameToColumnNumber.get(columnName); if (index == null) { - throw new SQLException(String.format("There is no column with name %s ", columnName)); + throw new SQLException(format("There is no column with name %s ", columnName)); } return index; } @Override - public boolean isWrapperFor(Class iface) { - return iface.isAssignableFrom(getClass()); - } - - @Override - public T unwrap(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return iface.cast(this); - } - throw new SQLException("Cannot unwrap to " + iface.getName()); - } - - @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getAsciiStream(int columnIndex) throws SQLException { - throw new FireboltUnsupportedOperationException(); + return getTextStream(columnIndex, StandardCharsets.US_ASCII); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getUnicodeStream(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getTextStream(columnIndex, StandardCharsets.UTF_8); + } + + private InputStream getTextStream(int columnIndex, Charset charset) throws SQLException { + return ofNullable(getString(columnIndex)).map(str -> str.getBytes(charset)).map(ByteArrayInputStream::new).orElse(null); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getBinaryStream(int columnIndex) throws SQLException { - throw new FireboltUnsupportedOperationException(); + return ofNullable(getBytes(columnIndex)).map(ByteArrayInputStream::new).orElse(null); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getAsciiStream(String columnLabel) throws SQLException { - throw new FireboltUnsupportedOperationException(); + return getAsciiStream(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getUnicodeStream(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getUnicodeStream(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public InputStream getBinaryStream(String columnLabel) throws SQLException { - throw new FireboltUnsupportedOperationException(); - } - - @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public SQLWarning getWarnings() throws SQLException { - throw new FireboltUnsupportedOperationException(); - } - - @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public void clearWarnings() throws SQLException { - throw new FireboltUnsupportedOperationException(); + return getBinaryStream(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public String getCursorName() throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Reader getCharacterStream(int columnIndex) throws SQLException { - throw new FireboltUnsupportedOperationException(); + return ofNullable(getUnicodeStream(columnIndex)).map(InputStreamReader::new).orElse(null); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Reader getCharacterStream(String columnLabel) throws SQLException { - throw new FireboltUnsupportedOperationException(); + return getCharacterStream(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public void beforeFirst() throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "beforeFirst")); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public void afterLast() throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "afterLast")); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean absolute(int row) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "absolute")); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean relative(int rows) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "relative")); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean previous() throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + throw new FireboltException(format(FORWARD_ONLY_ERROR, "previous")); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public int getFetchDirection() throws SQLException { - throw new FireboltUnsupportedOperationException(); + public int getFetchDirection() { + return ResultSet.FETCH_FORWARD; } @Override public void setFetchDirection(int direction) throws SQLException { - throw new FireboltUnsupportedOperationException(); + if (direction != ResultSet.FETCH_FORWARD) { + throw new FireboltException(ExceptionType.TYPE_NOT_SUPPORTED); + } } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public int getFetchSize() throws SQLException { - throw new FireboltUnsupportedOperationException(); + @SuppressWarnings("SpellCheckingInspection") + public int getFetchSize() { + return 0; // fetch size is not supported; 0 means unlimited like in PostgreSQL and MySQL } @Override @@ -702,141 +650,117 @@ public void setFetchSize(int rows) throws SQLException { } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public int getConcurrency() throws SQLException { - throw new FireboltUnsupportedOperationException(); + public int getConcurrency() { + return ResultSet.CONCUR_READ_ONLY; } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean rowUpdated() throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean rowInserted() throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public boolean rowDeleted() throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNull(int columnIndex) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBoolean(int columnIndex, boolean x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateByte(int columnIndex, byte x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateShort(int columnIndex, short x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateInt(int columnIndex, int x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateLong(int columnIndex, long x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateFloat(int columnIndex, float x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateDouble(int columnIndex, double x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateString(int columnIndex, String x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBytes(int columnIndex, byte[] x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateDate(int columnIndex, Date x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateTime(int columnIndex, Time x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @@ -850,63 +774,54 @@ public void updateCharacterStream(int columnIndex, Reader x, int length) throws @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateObject(int columnIndex, Object x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNull(String columnLabel) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBoolean(String columnLabel, boolean x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateByte(String columnLabel, byte x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateShort(String columnLabel, short x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateInt(String columnLabel, int x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateLong(String columnLabel, long x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateFloat(String columnLabel, float x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @@ -920,77 +835,66 @@ public void updateDouble(String columnLabel, double x) throws SQLException { @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateString(String columnLabel, String x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBytes(String columnLabel, byte[] x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateDate(String columnLabel, Date x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateTime(String columnLabel, Time x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateObject(String columnLabel, Object x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @@ -1045,17 +949,26 @@ public void moveToCurrentRow() throws SQLException { } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public Statement getStatement() throws SQLException { - throw new FireboltUnsupportedOperationException(); + public Statement getStatement() { + return statement; } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Object getObject(int columnIndex, Map> map) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + FireboltDataType dataType = resultSetMetaData.getColumn(columnIndex).getType().getDataType(); + Map> caseInsensitiveMap = new TreeMap<>(CASE_INSENSITIVE_ORDER); + caseInsensitiveMap.putAll(map); + Class type = getAllNames(dataType).map(caseInsensitiveMap::get).filter(Objects::nonNull).findFirst() + .orElseThrow(() -> new FireboltException(format("Cannot find type %s in provided types map", dataType))); + return getObject(columnIndex, type); + } + + private Stream getAllNames(FireboltDataType dataType) { + return Stream.concat(Stream.of(dataType.getDisplayName(), getJdbcType(dataType)).filter(Objects::nonNull), Stream.of(dataType.getAliases())); + } + + private String getJdbcType(FireboltDataType dataType) { + return JDBCType.valueOf(dataType.getSqlType()).getName(); } @Override @@ -1066,24 +979,18 @@ public Ref getRef(int columnIndex) throws SQLException { } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Blob getBlob(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return Optional.ofNullable(getBytes(columnIndex)).map(FireboltBlob::new).orElse(null); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Clob getClob(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return Optional.ofNullable(getString(columnIndex)).map(String::toCharArray).map(FireboltClob::new).orElse(null); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Object getObject(String columnLabel, Map> map) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getObject(findColumn(columnLabel), map); } @Override @@ -1094,85 +1001,77 @@ public Ref getRef(String columnLabel) throws SQLException { } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Blob getBlob(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getBlob(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Clob getClob(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getClob(findColumn(columnLabel)); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public URL getURL(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); - } + return createURL(getString(columnIndex)); + } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public URL getURL(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return createURL(getString(columnLabel)); + } + + private URL createURL(String url) throws SQLException { + try { + return url == null ? null : new URL(url); + } catch (MalformedURLException e) { + throw new SQLException(e); + } } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateRef(int columnIndex, Ref x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateRef(String columnLabel, Ref x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(int columnIndex, Blob x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(String columnLabel, Blob x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(int columnIndex, Clob x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(String columnLabel, Clob x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateArray(int columnIndex, Array x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateArray(String columnLabel, Array x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @@ -1193,65 +1092,54 @@ public RowId getRowId(String columnLabel) throws SQLException { @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateRowId(int columnIndex, RowId x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateRowId(String columnLabel, RowId x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport - public int getHoldability() throws SQLException { - throw new FireboltUnsupportedOperationException(); + public int getHoldability() { + return ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNString(int columnIndex, String nString) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNString(String columnLabel, String nString) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(int columnIndex, NClob nClob) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(String columnLabel, NClob nClob) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public NClob getNClob(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + String str = getString(columnIndex); + return str == null ? null : new FireboltClob(str.toCharArray()); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public NClob getNClob(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getNClob(findColumn(columnLabel)); } @Override @@ -1270,240 +1158,201 @@ public SQLXML getSQLXML(String columnLabel) throws SQLException { @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public String getNString(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getString(columnIndex); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public String getNString(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getString(columnLabel); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Reader getNCharacterStream(int columnIndex) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getCharacterStream(columnIndex); } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public Reader getNCharacterStream(String columnLabel) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + return getCharacterStream(columnLabel); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(int columnIndex, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateClob(String columnLabel, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(int columnIndex, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } + @Override @NotImplemented - @ExcludeFromJacocoGeneratedReport public void updateNClob(String columnLabel, Reader reader) throws SQLException { throw new FireboltSQLFeatureNotSupportedException(); } - - } diff --git a/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaData.java b/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaData.java index 51ea755f..ae7449ea 100644 --- a/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaData.java +++ b/src/main/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaData.java @@ -1,31 +1,29 @@ package com.firebolt.jdbc.resultset; +import com.firebolt.jdbc.GenericWrapper; +import com.firebolt.jdbc.resultset.column.Column; + import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.List; +import java.util.Objects; -import org.apache.commons.lang3.StringUtils; - -import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; -import com.firebolt.jdbc.annotation.NotImplemented; -import com.firebolt.jdbc.exception.FireboltException; -import com.firebolt.jdbc.resultset.column.Column; +import static java.lang.String.format; -import lombok.Builder; -import lombok.EqualsAndHashCode; -import lombok.Value; +@SuppressWarnings("java:S4144") // Methods should not have identical implementations - many methods here have trivial identical implementation +public class FireboltResultSetMetaData implements ResultSetMetaData, GenericWrapper { + private final String dbName; + private final String tableName; + private final List columns; -@Value -@Builder -@EqualsAndHashCode(callSuper = false) -public class FireboltResultSetMetaData implements ResultSetMetaData { - - List columns; - String tableName; - String dbName; + public FireboltResultSetMetaData(String dbName, String tableName, List columns) { + this.dbName = dbName; + this.tableName = tableName; + this.columns = columns; + } @Override - public int getColumnCount() throws SQLException { + public int getColumnCount() { return columns.size(); } @@ -61,12 +59,14 @@ public int getScale(int column) throws SQLException { @Override public String getTableName(int column) throws SQLException { - return this.tableName; + checkColumnNumber(column); + return tableName; } @Override public String getCatalogName(int column) throws SQLException { - return this.dbName; + checkColumnNumber(column); + return dbName; } @Override @@ -84,22 +84,9 @@ public String getColumnClassName(int column) throws SQLException { return getColumn(column).getType().getDataType().getBaseType().getType().getName(); } - public Column getColumn(int column) { - return this.columns.get(column - 1); - } - - @Override - @SuppressWarnings("unchecked") - public T unwrap(Class interfaceName) throws SQLException { - if (isWrapperFor(interfaceName)) { - return (T) this; - } - throw new FireboltException("Unable unwrap to " + interfaceName); - } - - @Override - public boolean isWrapperFor(Class interfaceName) throws SQLException { - return interfaceName != null && interfaceName.isAssignableFrom(getClass()); + Column getColumn(int column) throws SQLException { + checkColumnNumber(column); + return columns.get(column - 1); } @Override @@ -108,52 +95,72 @@ public boolean isCaseSensitive(int column) throws SQLException { } @Override - @ExcludeFromJacocoGeneratedReport - @NotImplemented public boolean isAutoIncrement(int column) throws SQLException { + checkColumnNumber(column); return false; } @Override - @ExcludeFromJacocoGeneratedReport public boolean isSearchable(int column) throws SQLException { + checkColumnNumber(column); return true; } @Override - @ExcludeFromJacocoGeneratedReport public boolean isCurrency(int column) throws SQLException { + checkColumnNumber(column); return false; } @Override - @ExcludeFromJacocoGeneratedReport public int getColumnDisplaySize(int column) throws SQLException { - // Default value for backward compatibility - return 80; + checkColumnNumber(column); + return 80; // Default value for backward compatibility } @Override - @NotImplemented - @ExcludeFromJacocoGeneratedReport public String getSchemaName(int column) throws SQLException { - // Schemas are not implemented so N/A - return StringUtils.EMPTY; + checkColumnNumber(column); + return ""; // Schemas are not implemented so N/A } @Override public boolean isReadOnly(int column) throws SQLException { + checkColumnNumber(column); return true; } @Override - @ExcludeFromJacocoGeneratedReport public boolean isWritable(int column) throws SQLException { return !isReadOnly(column); } @Override - @ExcludeFromJacocoGeneratedReport public boolean isDefinitelyWritable(int column) throws SQLException { - return false; + return isWritable(column); + } + + + private void checkColumnNumber(int column) throws SQLException { + if (column < 1 || column > columns.size()) { + throw new SQLException(format("Invalid column number %d", column)); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FireboltResultSetMetaData that = (FireboltResultSetMetaData) o; + return Objects.equals(dbName, that.dbName) && Objects.equals(tableName, that.tableName) && Objects.equals(columns, that.columns); + } + + @Override + public int hashCode() { + return Objects.hash(dbName, tableName, columns); + } + + @Override + public String toString() { + return format("FireboltResultSetMetaData{dbName=%s tableName=%s, columns=%s}", dbName, tableName, columns); } } diff --git a/src/main/java/com/firebolt/jdbc/resultset/column/Column.java b/src/main/java/com/firebolt/jdbc/resultset/column/Column.java index 391f5690..8797c5e6 100644 --- a/src/main/java/com/firebolt/jdbc/resultset/column/Column.java +++ b/src/main/java/com/firebolt/jdbc/resultset/column/Column.java @@ -1,21 +1,24 @@ package com.firebolt.jdbc.resultset.column; import lombok.Builder; -import lombok.CustomLog; import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.ToString; + +import java.util.logging.Level; +import java.util.logging.Logger; @Builder @Getter @EqualsAndHashCode -@CustomLog +@ToString public final class Column { - + private static final Logger log = Logger.getLogger(Column.class.getName()); private final ColumnType type; private final String columnName; public static Column of(String columnType, String columnName) { - log.debug("Creating column info for column: {} of type: {}", columnName, columnType); + log.log(Level.FINE, "Creating column info for column: {0} of type: {1}", new Object[] {columnName, columnType}); return Column.builder().columnName(columnName).type(ColumnType.of(columnType)).build(); } } diff --git a/src/main/java/com/firebolt/jdbc/resultset/column/ColumnType.java b/src/main/java/com/firebolt/jdbc/resultset/column/ColumnType.java index b87d3c53..c8ddf673 100644 --- a/src/main/java/com/firebolt/jdbc/resultset/column/ColumnType.java +++ b/src/main/java/com/firebolt/jdbc/resultset/column/ColumnType.java @@ -1,33 +1,46 @@ package com.firebolt.jdbc.resultset.column; -import static com.firebolt.jdbc.type.FireboltDataType.*; - -import java.util.*; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.RegExUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.type.FireboltDataType; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.Value; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; -import lombok.*; +import static com.firebolt.jdbc.type.FireboltDataType.ARRAY; +import static com.firebolt.jdbc.type.FireboltDataType.TUPLE; +import static com.firebolt.jdbc.type.FireboltDataType.ofType; /** * This class represents a Column type returned by the server */ -@CustomLog @Builder @Value @EqualsAndHashCode public class ColumnType { private static final String NOT_NULLABLE_TYPE = "NOT NULL"; private static final String NULL_TYPE = "NULL"; + private static final String NOT_NULLABLE_TYPE_SUFFIX = " " + NOT_NULLABLE_TYPE; + private static final String NULL_TYPE_SUFFIX = " " + NULL_TYPE; private static final String NULLABLE_TYPE = "NULLABLE"; private static final Set TIMEZONES = Arrays.stream(TimeZone.getAvailableIDs()) .collect(Collectors.toCollection(HashSet::new)); + private static final Pattern COMMA_WITH_SPACES = Pattern.compile("\\s*,\\s*"); + private static final Logger log = Logger.getLogger(ColumnType.class.getName()); @EqualsAndHashCode.Exclude String name; FireboltDataType dataType; @@ -40,14 +53,14 @@ public class ColumnType { public static ColumnType of(String columnType) { List innerDataTypes = null; TimeZone timeZone = null; - Optional, Optional>> scaleAndPrecisionPair; + Optional, Optional>> scaleAndPrecisionPair; FireboltDataType fireboltType; ColumnTypeWrapper columnTypeWrapper = ColumnTypeWrapper.of(columnType); String typeWithoutNullKeyword = columnTypeWrapper.getTypeWithoutNullKeyword(); boolean isNullable = columnTypeWrapper.isNullable(); if (isType(FireboltDataType.ARRAY, typeWithoutNullKeyword) ) { - innerDataTypes = getCollectionSubType(FireboltDataType.ARRAY, typeWithoutNullKeyword); + innerDataTypes = getCollectionSubType(FireboltDataType.ARRAY, typeWithoutNullKeyword); } else if (isType(FireboltDataType.TUPLE, typeWithoutNullKeyword)) { innerDataTypes = getCollectionSubType(FireboltDataType.TUPLE, typeWithoutNullKeyword); } @@ -63,13 +76,13 @@ public static ColumnType of(String columnType) { } else { scaleAndPrecisionPair = Optional.empty(); } - if (dataType.isTime() && ArrayUtils.isNotEmpty(arguments)) { + if (dataType.isTime() && arguments != null && arguments.length > 0) { timeZone = getTimeZoneFromArguments(arguments); } return builder().name(columnTypeWrapper.getTypeInUpperCase()).nullable(isNullable).dataType(fireboltType) - .scale(scaleAndPrecisionPair.map(Pair::getLeft).filter(Optional::isPresent).map(Optional::get) + .scale(scaleAndPrecisionPair.map(Entry::getKey).filter(Optional::isPresent).map(Optional::get) .orElse(dataType.getMaxScale())) - .precision(scaleAndPrecisionPair.map(Pair::getRight).filter(Optional::isPresent).map(Optional::get) + .precision(scaleAndPrecisionPair.map(Entry::getValue).filter(Optional::isPresent).map(Optional::get) .orElse(dataType.getPrecision())) .timeZone(timeZone).innerTypes(Optional.ofNullable(innerDataTypes).orElse(new ArrayList<>())).build(); } @@ -86,9 +99,9 @@ private static boolean isType(FireboltDataType fireboltDataType, String typeWith private static List getCollectionSubType(FireboltDataType fireboltDataType, String typeWithoutNullKeyword) { String[] types; for (String type: fireboltDataType.getAliases()) { - if(typeWithoutNullKeyword.startsWith(type.toUpperCase())) { - typeWithoutNullKeyword = RegExUtils.replaceFirst(typeWithoutNullKeyword, type.toUpperCase() + "\\(", ""); - typeWithoutNullKeyword = StringUtils.substring(typeWithoutNullKeyword, 0, typeWithoutNullKeyword.length() - 1); + String typeUpperCase = type.toUpperCase(); + if (typeWithoutNullKeyword.startsWith(typeUpperCase)) { + typeWithoutNullKeyword = typeWithoutNullKeyword.substring((typeUpperCase + "\\(").length() - 1, typeWithoutNullKeyword.length() - 1); break; } } @@ -115,8 +128,7 @@ private static int getTypeEndPosition(String type) { return typeNameEndIndex < 0 ? type.length() : typeNameEndIndex; } - private static Pair, Optional> getsCaleAndPrecision(String[] arguments, - FireboltDataType dataType) { + private static Entry, Optional> getsCaleAndPrecision(String[] arguments, FireboltDataType dataType) { Integer scale = null; Integer precision = null; switch (dataType) { @@ -135,11 +147,11 @@ private static Pair, Optional> getsCaleAndPrecision(S default: break; } - return new ImmutablePair<>(Optional.ofNullable(scale), Optional.ofNullable(precision)); + return Map.entry(Optional.ofNullable(scale), Optional.ofNullable(precision)); } private static String[] splitArguments(String args, int index) { - return StringUtils.substring(args, args.indexOf("(", index) + 1, args.indexOf(")", index)).split("\\s*,\\s*"); + return COMMA_WITH_SPACES.split(args.substring(args.indexOf("(", index) + 1, args.indexOf(")", index))); } private static TimeZone getTimeZoneFromArguments(@NonNull String[] arguments) { @@ -147,7 +159,7 @@ private static TimeZone getTimeZoneFromArguments(@NonNull String[] arguments) { TimeZone timeZone = null; if (arguments.length > 1) { timeZoneArgument = arguments[1]; - } else if (arguments.length == 1 && !StringUtils.isNumeric(arguments[0])) { + } else if (arguments.length == 1 && arguments[0].chars().anyMatch(c -> !Character.isDigit(c))) { timeZoneArgument = arguments[0]; } if (timeZoneArgument != null) { @@ -155,18 +167,17 @@ private static TimeZone getTimeZoneFromArguments(@NonNull String[] arguments) { if (TIMEZONES.contains(id)) { timeZone = TimeZone.getTimeZone(timeZoneArgument.replace("\\'", "")); } else { - log.warn("Could not use the timezone returned by the server with the id {} as it is not supported.", - id); + log.log(Level.FINE, "Could not use the timezone returned by the server with the id {0} as it is not supported.", id); } } return timeZone; } public String getCompactTypeName() { - if (this.isArray()) { + if (isArray()) { return getArrayCompactTypeName(); - } else if (this.isTuple()) { - return getTupleCompactTypeName(this.innerTypes); + } else if (isTuple()) { + return getTupleCompactTypeName(innerTypes); } else { return dataType.getDisplayName(); } @@ -178,13 +189,11 @@ private String getArrayCompactTypeName() { ColumnType columnType = this; while (columnType != null && columnType.getDataType() == ARRAY) { depth++; - compactType.append(ARRAY.getDisplayName() + "("); + compactType.append(ARRAY.getDisplayName()).append("("); columnType = columnType.getInnerTypes().isEmpty() ? null : columnType.getInnerTypes().get(0); } - compactType.append(this.getArrayBaseColumnType().getCompactTypeName()); - for (int i = 0; i < depth; i++) { - compactType.append(")"); - } + compactType.append(getArrayBaseColumnType().getCompactTypeName()); + compactType.append(")".repeat(depth)); return compactType.toString(); } @@ -205,7 +214,7 @@ public ColumnType getArrayBaseColumnType() { if (innerTypes == null || innerTypes.isEmpty()) { return null; } - ColumnType currentInnerType = this.innerTypes.get(0); + ColumnType currentInnerType = innerTypes.get(0); while (currentInnerType.getInnerTypes() != null && !currentInnerType.getInnerTypes().isEmpty() && currentInnerType.getDataType() != TUPLE) { currentInnerType = currentInnerType.getInnerTypes().get(0); @@ -223,21 +232,20 @@ private static class ColumnTypeWrapper { public static ColumnTypeWrapper of(String type) { boolean isNullable = false; - String typeInUpperCase = StringUtils.upperCase(type); + String typeInUpperCase = type == null ? null : type.toUpperCase(); String typeWithoutNullableKeyword = typeInUpperCase; if (typeInUpperCase.startsWith(NULLABLE_TYPE)) { isNullable = true; - typeWithoutNullableKeyword = typeInUpperCase.substring(NULLABLE_TYPE.length() + 1, - typeInUpperCase.length() - 1); - } else if (typeInUpperCase.endsWith(NOT_NULLABLE_TYPE)) { - typeWithoutNullableKeyword = StringUtils.removeEnd(typeInUpperCase, " " + NOT_NULLABLE_TYPE); + typeWithoutNullableKeyword = typeInUpperCase.substring(NULLABLE_TYPE.length() + 1, typeInUpperCase.length() - 1); + } else if (typeInUpperCase.endsWith(NOT_NULLABLE_TYPE_SUFFIX)) { + typeWithoutNullableKeyword = typeInUpperCase.substring(0, typeInUpperCase.length() - NOT_NULLABLE_TYPE_SUFFIX.length()); + } else if (typeInUpperCase.endsWith(NULL_TYPE_SUFFIX)) { + isNullable = true; + typeWithoutNullableKeyword = typeInUpperCase.substring(0, typeInUpperCase.length() - NULL_TYPE_SUFFIX.length()); } else if (typeInUpperCase.endsWith(NULL_TYPE)) { isNullable = true; - typeWithoutNullableKeyword = StringUtils.removeEnd(typeInUpperCase, " " + NULL_TYPE); } return new ColumnTypeWrapper(type, typeInUpperCase, typeWithoutNullableKeyword, isNullable); - } } - } diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltAccountIdService.java b/src/main/java/com/firebolt/jdbc/service/FireboltAccountIdService.java new file mode 100644 index 00000000..fcdefbd0 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltAccountIdService.java @@ -0,0 +1,18 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; + +import java.sql.SQLException; + +public class FireboltAccountIdService { + private final FireboltAccountRetriever firebolAccountClient; + + public FireboltAccountIdService(FireboltAccountRetriever firebolAccountClient) { + this.firebolAccountClient = firebolAccountClient; + } + + public FireboltAccount getValue(String accessToken, String account) throws SQLException { + return firebolAccountClient.retrieve(accessToken, account); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java index 56b17727..a03c2786 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltAuthenticationService.java @@ -4,61 +4,60 @@ import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; -import lombok.CustomLog; +import com.firebolt.jdbc.exception.SQLState; + import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; -import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; -import org.apache.commons.lang3.StringUtils; -import javax.xml.bind.DatatypeConverter; +import java.math.BigInteger; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Optional; -import java.util.concurrent.TimeUnit; +import java.sql.SQLException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.lang.String.format; +import static java.util.Optional.ofNullable; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.jodah.expiringmap.ExpirationPolicy.CREATED; @RequiredArgsConstructor -@CustomLog public class FireboltAuthenticationService { + private static final Logger log = Logger.getLogger(FireboltAuthenticationService.class.getName()); private static final ExpiringMap tokensMap = ExpiringMap.builder() .variableExpiration().build(); private static final long TOKEN_EXPIRATION_OFFSET = 5L; private static final long TOKEN_TTL_THRESHOLD = 60L; + private static final String ERROR_MESSAGE = "Failed to connect to Firebolt with the error: %s, see logs for more info."; + private static final String ERROR_MESSAGE_FROM_SERVER = "Failed to connect to Firebolt with the error from the server: %s, see logs for more info."; private final FireboltAuthenticationClient fireboltAuthenticationClient; - public FireboltConnectionTokens getConnectionTokens(String host, FireboltProperties loginProperties) throws FireboltException { + @SuppressWarnings("java:S2139") // TODO: Exceptions should be either logged or rethrown but not both + public FireboltConnectionTokens getConnectionTokens(String host, FireboltProperties loginProperties) throws SQLException { try { - ConnectParams connectionParams = new ConnectParams(host, loginProperties.getUser(), - loginProperties.getPassword()); + ConnectParams connectionParams = new ConnectParams(host, loginProperties.getPrincipal(), loginProperties.getSecret()); synchronized (this) { FireboltConnectionTokens foundToken = tokensMap.get(connectionParams); if (foundToken != null) { - log.debug("Using the token of {} from the cache", host); + log.log(Level.FINE, "Using the token of {} from the cache", host); return foundToken; - } else { - FireboltConnectionTokens fireboltConnectionTokens = fireboltAuthenticationClient - .postConnectionTokens(host, loginProperties.getUser(), loginProperties.getPassword()); - long durationInSeconds = getCachingDurationInSeconds( - fireboltConnectionTokens.getExpiresInSeconds()); - tokensMap.put(connectionParams, fireboltConnectionTokens, ExpirationPolicy.CREATED, - durationInSeconds, TimeUnit.SECONDS); - return fireboltConnectionTokens; } + FireboltConnectionTokens fireboltConnectionTokens = fireboltAuthenticationClient + .postConnectionTokens(host, loginProperties.getPrincipal(), loginProperties.getSecret(), loginProperties.getEnvironment()); + long durationInSeconds = getCachingDurationInSeconds(fireboltConnectionTokens.getExpiresInSeconds()); + tokensMap.put(connectionParams, fireboltConnectionTokens, CREATED, durationInSeconds, SECONDS); + return fireboltConnectionTokens; } + } catch (FireboltException e) { + log.log(Level.SEVERE, "Failed to connect to Firebolt", e); + String msg = ofNullable(e.getErrorMessageFromServer()).map(m -> format(ERROR_MESSAGE_FROM_SERVER, m)).orElse(format(ERROR_MESSAGE, e.getMessage())); + SQLState sqlState = SQLState.fromCode(e.getSQLState()); + throw new FireboltException(msg, e, sqlState); } catch (Exception e) { - log.error("Failed to connect to Firebolt", e); - if (e instanceof FireboltException - && StringUtils.isNotEmpty(((FireboltException) e).getErrorMessageFromServer())) { - throw new FireboltException(String.format( - "Failed to connect to Firebolt with the error from the server: %s, see logs for more info.", - ((FireboltException) e).getErrorMessageFromServer()), e); - } else { - throw new FireboltException( - String.format("Failed to connect to Firebolt with the error: %s, see logs for more info.", - e.getMessage()), - e); - } + log.log(Level.SEVERE, "Failed to connect to Firebolt", e); + throw new FireboltException(format(ERROR_MESSAGE, e.getMessage()), e); } } @@ -73,15 +72,14 @@ private long getCachingDurationInSeconds(long expireInSeconds) { /** * Removes connection tokens from the cache. - * + * * @param host host * @param loginProperties the login properties linked to the tokens */ - public void removeConnectionTokens(String host, FireboltProperties loginProperties) throws FireboltException { + public void removeConnectionTokens(String host, FireboltProperties loginProperties) throws SQLException { try { - log.debug("Removing connection token for host {}", host); - ConnectParams connectionParams = new ConnectParams(host, loginProperties.getUser(), - loginProperties.getPassword()); + log.log(Level.FINE, "Removing connection token for host {0}", host); + ConnectParams connectionParams = new ConnectParams(host, loginProperties.getPrincipal(), loginProperties.getSecret()); tokensMap.remove(connectionParams); } catch (NoSuchAlgorithmException e) { throw new FireboltException("Could not remove connection tokens", e); @@ -93,12 +91,12 @@ private static class ConnectParams { public final String fireboltHost; public final String credentialsHash; - public ConnectParams(String fireboltHost, String user, String password) throws NoSuchAlgorithmException { + public ConnectParams(String fireboltHost, String principal, String secret) throws NoSuchAlgorithmException { this.fireboltHost = fireboltHost; MessageDigest sha256Instance = MessageDigest.getInstance("SHA-256"); - Optional.ofNullable(user).map(String::getBytes).ifPresent(sha256Instance::update); - Optional.ofNullable(password).map(String::getBytes).ifPresent(sha256Instance::update); - this.credentialsHash = DatatypeConverter.printHexBinary(sha256Instance.digest()); + ofNullable(principal).map(String::getBytes).ifPresent(sha256Instance::update); + ofNullable(secret).map(String::getBytes).ifPresent(sha256Instance::update); + this.credentialsHash = new BigInteger(1, sha256Instance.digest()).toString(16); } } } diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java new file mode 100644 index 00000000..4c2be433 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineApiService.java @@ -0,0 +1,106 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import lombok.RequiredArgsConstructor; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.Optional; +import java.util.Set; + +import static java.lang.String.format; +import static java.util.Optional.ofNullable; + +@RequiredArgsConstructor +public class FireboltEngineApiService implements FireboltEngineService { + private static final Set ENGINE_NOT_READY_STATUSES = Set.of( + "ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_PENDING", + "ENGINE_STATUS_PROVISIONING_FINISHED", "ENGINE_STATUS_RUNNING_REVISION_STARTING"); + private static final String ERROR_NO_ENGINE_ATTACHED = "There is no Firebolt engine running on %s attached to the database %s. To connect first make sure there is a running engine and then try again."; + private static final String ERROR_NO_ENGINE_WITH_NAME = "There is no Firebolt engine running on %s with the name %s. To connect first make sure there is a running engine and then try again."; + + + private final FireboltAccountClient fireboltAccountClient; + + @Override + public Engine getEngine(FireboltProperties properties) throws SQLException { + return getEngine(properties.getHttpConnectionUrl(), properties, properties.getAccessToken()); + } + + /** + * Returns the engine + * + * @param connectionUrl the connection url + * @param loginProperties properties to login + * @param accessToken the access token + * @return the engine + */ + private Engine getEngine(String connectionUrl, FireboltProperties loginProperties, String accessToken) throws SQLException { + String accountId = null; + Engine engine; + try { + if (loginProperties.getAccount() != null && !loginProperties.getAccount().isEmpty()) { + accountId = getAccountId(connectionUrl, loginProperties.getAccount(), accessToken).orElse(null); + } + if (loginProperties.getEngine() == null || loginProperties.getEngine().isEmpty()) { + engine = getDefaultEngine(connectionUrl, accountId, loginProperties.getDatabase(), accessToken); + } else { + engine = getEngineWithName(connectionUrl, accountId, loginProperties.getEngine(), accessToken); + } + } catch (FireboltException e) { + throw e; + } catch (Exception e) { + throw new FireboltException("Failed to get engine", e); + } + validateEngineIsNotStarting(engine); + return engine; + } + + private Engine getEngineWithName(String connectionUrl, String accountId, String engineName, String accessToken) throws SQLException, IOException { + FireboltEngineIdResponse response = fireboltAccountClient.getEngineId(connectionUrl, accountId, engineName, + accessToken); + String engineID = ofNullable(response).map(FireboltEngineIdResponse::getEngine) + .map(FireboltEngineIdResponse.Engine::getEngineId).orElseThrow(() -> new FireboltException( + "Failed to extract engine id field from the server response: the response from the server is invalid.")); + FireboltEngineResponse fireboltEngineResponse = fireboltAccountClient.getEngine(connectionUrl, accountId, + engineName, engineID, accessToken); + + return ofNullable(fireboltEngineResponse).map(FireboltEngineResponse::getEngine) + .filter(e -> e.getEndpoint() != null) + .filter(e -> !e.getEndpoint().isEmpty()) + .map(e -> new Engine(e.getEndpoint(), e.getCurrentStatus(), engineName, null, engineID)) + .orElseThrow(() -> new FireboltException( + format(ERROR_NO_ENGINE_WITH_NAME, connectionUrl, engineName))); + } + + private Engine getDefaultEngine(String connectionUrl, String accountId, String database, String accessToken) throws SQLException, IOException { + FireboltDefaultDatabaseEngineResponse defaultEngine = fireboltAccountClient + .getDefaultEngineByDatabaseName(connectionUrl, accountId, database, accessToken); + + return ofNullable(defaultEngine).map(FireboltDefaultDatabaseEngineResponse::getEngineUrl) + .map(url -> new Engine(url, "running", null, database, null)).orElseThrow( + () -> new FireboltException(format(ERROR_NO_ENGINE_ATTACHED, connectionUrl, database))); + } + + private Optional getAccountId(String connectionUrl, String account, String accessToken) + throws SQLException, IOException { + return ofNullable(fireboltAccountClient.getAccount(connectionUrl, account, accessToken)).map(FireboltAccountResponse::getAccountId); + } + + private void validateEngineIsNotStarting(Engine engine) throws SQLException { + String id = engine.getId(); + String status = engine.getStatus(); + if (id != null && !id.isEmpty() && status != null && !status.isEmpty() && ENGINE_NOT_READY_STATUSES.contains(engine.getStatus())) { + throw new FireboltException(format( + "The engine %s is currently starting. Please wait until the engine is on and then execute the query again.", engine.getName())); + } + } + +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java new file mode 100644 index 00000000..731663a0 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaService.java @@ -0,0 +1,86 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; +import java.util.TreeSet; +import java.util.stream.Stream; + +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.lang.String.format; +import static java.util.stream.Collectors.toCollection; + +public class FireboltEngineInformationSchemaService implements FireboltEngineService { + private static final String ENGINE_URL = "url"; + private static final String STATUS_FIELD = "status"; + private static final String ENGINE_NAME_FIELD = "engine_name"; + private static final Collection RUNNING_STATUSES = Stream.of("running", "ENGINE_STATE_RUNNING").collect(toCollection(() -> new TreeSet<>(CASE_INSENSITIVE_ORDER))); + private static final String ENGINE_QUERY = + "SELECT engs.url, engs.attached_to, dbs.%1$s_name, engs.status, engs.engine_name " + + "FROM information_schema.engines as engs " + + "LEFT JOIN information_schema.%1$ss as dbs ON engs.attached_to = dbs.%1$s_name " + + "WHERE engs.engine_name = ?"; + private static final String INVENTORY_QUERY = "SELECT %1$s_name FROM information_schema.%1$ss WHERE %1$s_name=?"; + + private final FireboltConnection fireboltConnection; + private final String dbTerm; + + public FireboltEngineInformationSchemaService(FireboltConnection fireboltConnection) throws SQLException { + this.fireboltConnection = fireboltConnection; + dbTerm = doesRecordExist(format(INVENTORY_QUERY, "table"), "catalogs") ? "catalog" : "database"; + } + + @Override + public boolean doesDatabaseExist(String database) throws SQLException { + return doesRecordExist(format(INVENTORY_QUERY, dbTerm), database); + } + + private boolean doesRecordExist(String query, String param) throws SQLException { + try (PreparedStatement ps = fireboltConnection.prepareStatement(query)) { + ps.setString(1, param); + try (ResultSet rs = ps.executeQuery()) { + return rs.next(); + } + } + } + + @Override + public Engine getEngine(FireboltProperties properties) throws SQLException { + String engine = properties.getEngine(); + String database = properties.getDatabase(); + if (engine == null) { + throw new IllegalArgumentException("Cannot retrieve engine parameters because its name is null"); + } + try (PreparedStatement ps = fireboltConnection.prepareStatement(format(ENGINE_QUERY, dbTerm))) { + ps.setString(1, engine); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + throw new FireboltException(format("The engine with the name %s could not be found", engine)); + } + String status = rs.getString(STATUS_FIELD); + if (!isEngineRunning(status)) { + throw new FireboltException(format("The engine with the name %s is not running. Status: %s", engine, status)); + } + String attachedDatabase = rs.getString("attached_to"); + if (attachedDatabase == null) { + throw new FireboltException(format("The engine with the name %s is not attached to any database", engine)); + } + if (database != null && !database.equals(attachedDatabase)) { + throw new FireboltException(format("The engine with the name %s is not attached to database %s", engine, database)); + } + String engineHost = properties.processEngineUrl(rs.getString(ENGINE_URL)); + return new Engine(engineHost, status, rs.getString(ENGINE_NAME_FIELD), attachedDatabase, null); + } + } + } + + private boolean isEngineRunning(String status) { + return RUNNING_STATUSES.contains(status); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java index 3ebac44b..b50c74b5 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineService.java @@ -1,117 +1,15 @@ package com.firebolt.jdbc.service; -import java.io.IOException; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Optional; -import java.util.Set; - -import org.apache.commons.lang3.StringUtils; - -import com.firebolt.jdbc.client.account.FireboltAccountClient; -import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; -import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; import com.firebolt.jdbc.connection.Engine; import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.exception.FireboltException; - -import lombok.CustomLog; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -@CustomLog -public class FireboltEngineService { - private static final Set ENGINE_NOT_READY_STATUSES = new HashSet<>( - Arrays.asList("ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_PENDING", - "ENGINE_STATUS_PROVISIONING_FINISHED", "ENGINE_STATUS_RUNNING_REVISION_STARTING")); - private static final String ERROR_NO_ENGINE_ATTACHED = "There is no Firebolt engine running on %s attached to the database %s. To connect first make sure there is a running engine and then try again."; - private static final String ERROR_NO_ENGINE_WITH_NAME = "There is no Firebolt engine running on %s with the name %s. To connect first make sure there is a running engine and then try again."; - private final FireboltAccountClient fireboltAccountClient; - - /** - * Returns the engine - * - * @param connectionUrl the connection url - * @param loginProperties properties to login - * @param accessToken the access token - * @return the engine - */ - public Engine getEngine(String connectionUrl, FireboltProperties loginProperties, String accessToken) - throws FireboltException { - String accountId = null; - Engine engine; - try { - if (StringUtils.isNotEmpty(loginProperties.getAccount())) { - accountId = getAccountId(connectionUrl, loginProperties.getAccount(), accessToken).orElse(null); - } - if (StringUtils.isEmpty(loginProperties.getEngine())) { - engine = getDefaultEngine(connectionUrl, accountId, loginProperties.getDatabase(), accessToken); - } else { - engine = getEngineWithName(connectionUrl, accountId, loginProperties.getEngine(), accessToken); - } - } catch (FireboltException e) { - throw e; - } catch (Exception e) { - throw new FireboltException("Failed to get engine", e); - } - validateEngineIsNotStarting(engine); - return engine; - } - - private Engine getEngineWithName(String connectionUrl, String accountId, String engineName, String accessToken) - throws FireboltException, IOException { - FireboltEngineIdResponse response = fireboltAccountClient.getEngineId(connectionUrl, accountId, engineName, - accessToken); - String engineID = Optional.ofNullable(response).map(FireboltEngineIdResponse::getEngine) - .map(FireboltEngineIdResponse.Engine::getEngineId).orElseThrow(() -> new FireboltException( - "Failed to extract engine id field from the server response: the response from the server is invalid.")); - FireboltEngineResponse fireboltEngineResponse = fireboltAccountClient.getEngine(connectionUrl, accountId, - engineName, engineID, accessToken); - - return Optional.ofNullable(fireboltEngineResponse).map(FireboltEngineResponse::getEngine) - .filter(e -> StringUtils.isNotEmpty(e.getEndpoint())) - .map(e -> Engine.builder().endpoint(e.getEndpoint()).id(engineID).status(e.getCurrentStatus()) - .name(engineName).build()) - .orElseThrow(() -> new FireboltException( - String.format(ERROR_NO_ENGINE_WITH_NAME, connectionUrl, engineName))); - } - - private Engine getDefaultEngine(String connectionUrl, String accountId, String database, String accessToken) - throws FireboltException, IOException { - FireboltDefaultDatabaseEngineResponse defaultEngine = fireboltAccountClient - .getDefaultEngineByDatabaseName(connectionUrl, accountId, database, accessToken); - return Optional.ofNullable(defaultEngine).map(FireboltDefaultDatabaseEngineResponse::getEngineUrl) - .map(url -> Engine.builder().endpoint(url).build()).orElseThrow( - () -> new FireboltException(String.format(ERROR_NO_ENGINE_ATTACHED, connectionUrl, database))); - } - private Optional getAccountId(String connectionUrl, String account, String accessToken) - throws FireboltException, IOException { - FireboltAccountResponse fireboltAccountResponse = fireboltAccountClient.getAccount(connectionUrl, account, - accessToken); - return Optional.ofNullable(fireboltAccountResponse).map(FireboltAccountResponse::getAccountId); - } +import java.sql.SQLException; - private void validateEngineIsNotStarting(Engine engine) throws FireboltException { - if (StringUtils.isNotEmpty(engine.getId()) && StringUtils.isNotEmpty(engine.getStatus()) - && ENGINE_NOT_READY_STATUSES.contains(engine.getStatus())) { - throw new FireboltException(String.format( - "The engine %s is currently starting. Please wait until the engine is on and then execute the query again.", engine.getName())); - } - } +public interface FireboltEngineService { + Engine getEngine(FireboltProperties properties) throws SQLException; - /** - * Extracts the engine name from host - * - * @param engineHost engine host - * @return the engine name - */ - public String getEngineNameFromHost(String engineHost) throws FireboltException { - return Optional.ofNullable(engineHost).filter(host -> host.contains(".")).map(host -> host.split("\\.")[0]) - .map(host -> host.replace("-", "_")).orElseThrow(() -> new FireboltException( - String.format("Could not establish the engine from the host: %s", engineHost))); - } + default boolean doesDatabaseExist(String database) throws SQLException { + return true; + } } diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltEngineVersion2Service.java b/src/main/java/com/firebolt/jdbc/service/FireboltEngineVersion2Service.java new file mode 100644 index 00000000..db15c199 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltEngineVersion2Service.java @@ -0,0 +1,36 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; + +import java.sql.SQLException; +import java.sql.Statement; + +import static java.lang.String.format; + +public class FireboltEngineVersion2Service implements FireboltEngineService { + private final FireboltConnection fireboltConnection; + + public FireboltEngineVersion2Service(FireboltConnection fireboltConnection) { + this.fireboltConnection = fireboltConnection; + } + + @Override + @SuppressWarnings("java:S2077") // Formatting SQL queries is security-sensitive - looks safe in this case + public Engine getEngine(FireboltProperties properties) throws SQLException { + try (Statement statement = fireboltConnection.createStatement()) { + if (properties.getDatabase() != null) { + statement.executeUpdate(use("DATABASE", properties.getDatabase())); + } + statement.executeUpdate(use("ENGINE", properties.getEngine())); + } + // now session properties are updated with new database and engine + FireboltProperties sessionProperties = fireboltConnection.getSessionProperties(); + return new Engine(fireboltConnection.getEndpoint(), null, sessionProperties.getEngine(), sessionProperties.getDatabase(), null); + } + + private String use(String entity, String name) { + return format("USE %s \"%s\"", entity, name); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltGatewayUrlService.java b/src/main/java/com/firebolt/jdbc/service/FireboltGatewayUrlService.java new file mode 100644 index 00000000..fef97d4b --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/service/FireboltGatewayUrlService.java @@ -0,0 +1,25 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import lombok.RequiredArgsConstructor; + +import java.sql.SQLException; + +@RequiredArgsConstructor +public class FireboltGatewayUrlService { + + private final FireboltAccountRetriever fireboltGatewayUrlClient; + + private String addProtocolToUrl(String url) { + if (!url.startsWith("http")) { + // assume secure connection + url = "https://" + url; + } + return url; + } + + public String getUrl(String accessToken, String account) throws SQLException { + return addProtocolToUrl(fireboltGatewayUrlClient.retrieve(accessToken, account).getEngineUrl()); + } +} diff --git a/src/main/java/com/firebolt/jdbc/service/FireboltStatementService.java b/src/main/java/com/firebolt/jdbc/service/FireboltStatementService.java index 2b2b8757..67d78300 100644 --- a/src/main/java/com/firebolt/jdbc/service/FireboltStatementService.java +++ b/src/main/java/com/firebolt/jdbc/service/FireboltStatementService.java @@ -1,53 +1,46 @@ package com.firebolt.jdbc.service; -import static com.firebolt.jdbc.exception.ExceptionType.INVALID_REQUEST; - -import java.io.InputStream; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.Optional; - import com.firebolt.jdbc.client.query.StatementClient; import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.statement.StatementInfoWrapper; - import com.firebolt.jdbc.statement.StatementType; import com.firebolt.jdbc.statement.rawstatement.QueryRawStatement; import com.firebolt.jdbc.util.CloseableUtil; import com.firebolt.jdbc.util.InputStreamUtil; -import lombok.CustomLog; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import java.io.InputStream; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Optional; + +import static java.util.Optional.ofNullable; + @RequiredArgsConstructor -@CustomLog public class FireboltStatementService { private static final String UNKNOWN_TABLE_NAME = "unknown"; private final StatementClient statementClient; - private final boolean systemEngine; /** * Executes statement * * @param statementInfoWrapper the statement info * @param properties the connection properties - * @param queryTimeout query timeout - * @param maxRows max rows - * @param standardSql indicates if standard sql should be used * @param statement the statement * @return an InputStream with the result */ public Optional execute(StatementInfoWrapper statementInfoWrapper, - FireboltProperties properties, int queryTimeout, int maxRows, boolean standardSql, - FireboltStatement statement) + FireboltProperties properties, FireboltStatement statement) throws SQLException { - InputStream is = statementClient.executeSqlStatement(statementInfoWrapper, properties, systemEngine, queryTimeout, standardSql); + int queryTimeout = statement.getQueryTimeout(); + boolean systemEngine = properties.isSystemEngine(); + InputStream is = statementClient.executeSqlStatement(statementInfoWrapper, properties, systemEngine, queryTimeout); if (statementInfoWrapper.getType() == StatementType.QUERY) { - return Optional.of(createResultSet(is, (QueryRawStatement) statementInfoWrapper.getInitialStatement(), properties, statement, maxRows)); + return Optional.of(createResultSet(is, (QueryRawStatement) statementInfoWrapper.getInitialStatement(), properties, statement)); } else { // If the statement is not a query, read all bytes from the input stream and close it. // This is needed otherwise the stream with the server will be closed after having received the first chunk of data (resulting in incomplete inserts). @@ -57,28 +50,20 @@ public Optional execute(StatementInfoWrapper statementInfoWrapper, return Optional.empty(); } - public void abortStatement(@NonNull String statementId, @NonNull FireboltProperties properties) - throws FireboltException { - if (systemEngine) { - throw new FireboltException("Cannot cancel a statement using a system engine", INVALID_REQUEST); - } else { - statementClient.abortStatement(statementId, properties); - } - } - - public void abortStatementHttpRequest(@NonNull String statementId) throws FireboltException { - statementClient.abortRunningHttpRequest(statementId); + public void abortStatement(@NonNull String statementLabel, @NonNull FireboltProperties properties) throws SQLException { + statementClient.abortStatement(statementLabel, properties); } - public boolean isStatementRunning(String statementId) { - return statementClient.isStatementRunning(statementId); + public boolean isStatementRunning(String statementLabel) { + return statementClient.isStatementRunning(statementLabel); } - private FireboltResultSet createResultSet(InputStream inputStream, QueryRawStatement initialQuery, FireboltProperties properties, FireboltStatement statement, int maxRows) + private FireboltResultSet createResultSet(InputStream inputStream, QueryRawStatement initialQuery, FireboltProperties properties, FireboltStatement statement) throws SQLException { - return new FireboltResultSet(inputStream, Optional.ofNullable(initialQuery.getTable()).orElse(UNKNOWN_TABLE_NAME), - Optional.ofNullable(initialQuery.getDatabase()).orElse(properties.getDatabase()), - properties.getBufferSize(), maxRows, properties.isCompress(), statement, - properties.isLogResultSet()); + return new FireboltResultSet(inputStream, + ofNullable(initialQuery.getTable()).orElse(UNKNOWN_TABLE_NAME), + ofNullable(initialQuery.getDatabase()).orElse(properties.getDatabase()), + properties.getBufferSize(), properties.isCompress(), + statement, properties.isLogResultSet()); } } diff --git a/src/main/java/com/firebolt/jdbc/statement/FireboltStatement.java b/src/main/java/com/firebolt/jdbc/statement/FireboltStatement.java index 536cb22f..caec200c 100644 --- a/src/main/java/com/firebolt/jdbc/statement/FireboltStatement.java +++ b/src/main/java/com/firebolt/jdbc/statement/FireboltStatement.java @@ -1,16 +1,15 @@ package com.firebolt.jdbc.statement; -import com.firebolt.jdbc.annotation.ExcludeFromJacocoGeneratedReport; +import com.firebolt.jdbc.JdbcBase; import com.firebolt.jdbc.annotation.NotImplemented; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException; import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException; import com.firebolt.jdbc.service.FireboltStatementService; import com.firebolt.jdbc.util.CloseableUtil; -import lombok.Builder; -import lombok.CustomLog; import java.io.InputStream; import java.sql.Connection; @@ -18,171 +17,164 @@ import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.stream.Collectors; +import java.util.logging.Level; +import java.util.logging.Logger; -@CustomLog -public class FireboltStatement implements Statement { +import static com.firebolt.jdbc.statement.rawstatement.StatementValidatorFactory.createValidator; +import static java.lang.String.format; +import static java.util.stream.Collectors.toCollection; +public class FireboltStatement extends JdbcBase implements Statement { + + private static final Logger log = Logger.getLogger(FireboltStatement.class.getName()); private final FireboltStatementService statementService; private final FireboltProperties sessionProperties; private final FireboltConnection connection; - private final Collection statementsToExecuteIds = new HashSet<>(); + private final Collection statementsToExecuteLabels = new HashSet<>(); private boolean closeOnCompletion = false; private int currentUpdateCount = -1; private int maxRows; + private int maxFieldSize; private volatile boolean isClosed = false; private StatementResultWrapper currentStatementResult; private StatementResultWrapper firstUnclosedStatementResult; - private int queryTimeout = 0; // zero means that there is not limit - private String runningStatementId; + private int queryTimeout = 0; // zero means that there is no limit + private String runningStatementLabel; + private final List batchStatements = new LinkedList<>(); - @Builder public FireboltStatement(FireboltStatementService statementService, FireboltProperties sessionProperties, FireboltConnection connection) { this.statementService = statementService; this.sessionProperties = sessionProperties; this.connection = connection; - log.debug("Created Statement"); + log.fine("Created Statement"); } @Override public ResultSet executeQuery(String sql) throws SQLException { - return this.executeQuery(StatementUtil.parseToStatementInfoWrappers(sql)); + return executeQuery(StatementUtil.parseToStatementInfoWrappers(sql)); } protected ResultSet executeQuery(List statementInfoList) throws SQLException { StatementInfoWrapper query = getOneQueryStatementInfo(statementInfoList); - Optional resultSet = this.execute(Collections.singletonList(query)); + Optional resultSet = execute(Collections.singletonList(query)); synchronized (this) { - if (!resultSet.isPresent()) { - throw new FireboltException("Could not return ResultSet - the query returned no result."); - } else { - return resultSet.get(); - } + return resultSet.orElseThrow(() -> new FireboltException("Could not return ResultSet - the query returned no result.")); } } @Override public boolean execute(String sql) throws SQLException { - return this.execute(StatementUtil.parseToStatementInfoWrappers(sql)).isPresent(); + return execute(StatementUtil.parseToStatementInfoWrappers(sql)).isPresent(); } protected Optional execute(List statements) throws SQLException { Optional resultSet = Optional.empty(); - this.closeAllResults(); - Set queryIds = statements.stream().map(StatementInfoWrapper::getId) - .collect(Collectors.toCollection(HashSet::new)); + closeAllResults(); + Set queryLabels = statements.stream().map(StatementInfoWrapper::getLabel).collect(toCollection(HashSet::new)); try { - synchronized (statementsToExecuteIds) { - statementsToExecuteIds.addAll(queryIds); + synchronized (statementsToExecuteLabels) { + statementsToExecuteLabels.addAll(queryLabels); } for (int i = 0; i < statements.size(); i++) { if (i == 0) { - resultSet = execute(statements.get(i), true, true); + resultSet = execute(statements.get(i)); } else { - execute(statements.get(i), true, true); + execute(statements.get(i)); } } } finally { - synchronized (statementsToExecuteIds) { - statementsToExecuteIds.removeAll(queryIds); + synchronized (statementsToExecuteLabels) { + statementsToExecuteLabels.removeAll(queryLabels); } } return resultSet; } - private Optional execute(StatementInfoWrapper statementInfoWrapper, boolean verifyNotCancelled, - boolean isStandardSql) throws SQLException { + @SuppressWarnings("java:S2139") // TODO: Exceptions should be either logged or rethrown but not both + private Optional execute(StatementInfoWrapper statementInfoWrapper) throws SQLException { + createValidator(statementInfoWrapper.getInitialStatement(), connection).validate(statementInfoWrapper.getInitialStatement()); ResultSet resultSet = null; - if (!verifyNotCancelled || isStatementNotCancelled(statementInfoWrapper)) { - runningStatementId = statementInfoWrapper.getId(); + if (isStatementNotCancelled(statementInfoWrapper)) { + runningStatementLabel = statementInfoWrapper.getLabel(); synchronized (this) { - this.validateStatementIsNotClosed(); + validateStatementIsNotClosed(); } InputStream inputStream = null; try { - log.info("Executing the statement with id {} : {}", statementInfoWrapper.getId(), - statementInfoWrapper.getSql()); + log.log(Level.FINE, "Executing the statement with label {0} : {1}", new Object[] {statementInfoWrapper.getLabel(), statementInfoWrapper.getSql()}); if (statementInfoWrapper.getType() == StatementType.PARAM_SETTING) { - this.connection.addProperty(statementInfoWrapper.getParam()); - log.debug("The property from the query {} was stored", runningStatementId); + connection.addProperty(statementInfoWrapper.getParam()); + log.log(Level.FINE, "The property from the query {0} was stored", runningStatementLabel); } else { - Optional currentRs = statementService.execute(statementInfoWrapper, - this.sessionProperties, this.queryTimeout, this.maxRows, isStandardSql, this); + Optional currentRs = statementService.execute(statementInfoWrapper, sessionProperties, this); if (currentRs.isPresent()) { resultSet = currentRs.get(); currentUpdateCount = -1; // Always -1 when returning a ResultSet } else { currentUpdateCount = 0; } - log.info("The query with the id {} was executed with success", runningStatementId); + log.log(Level.INFO, "The query with the label {0} was executed with success", runningStatementLabel); } } catch (Exception ex) { CloseableUtil.close(inputStream); - log.error(String.format("An error happened while executing the statement with the id %s", - runningStatementId), ex); + log.log(Level.SEVERE, ex, () -> format("An error happened while executing the statement with the id %s", runningStatementLabel)); throw ex; } finally { - runningStatementId = null; + runningStatementLabel = null; } synchronized (this) { - if (this.firstUnclosedStatementResult == null) { - this.firstUnclosedStatementResult = this.currentStatementResult = new StatementResultWrapper( - resultSet, statementInfoWrapper); + if (firstUnclosedStatementResult == null) { + firstUnclosedStatementResult = currentStatementResult = new StatementResultWrapper(resultSet, statementInfoWrapper); } else { - this.firstUnclosedStatementResult - .append(new StatementResultWrapper(resultSet, statementInfoWrapper)); + firstUnclosedStatementResult.append(new StatementResultWrapper(resultSet, statementInfoWrapper)); } } } else { - log.warn("Aborted query with id {}", statementInfoWrapper.getId()); + log.log(Level.FINE, "Aborted query with id {0}", statementInfoWrapper.getLabel()); } return Optional.ofNullable(resultSet); } private boolean isStatementNotCancelled(StatementInfoWrapper statementInfoWrapper) { - synchronized (statementsToExecuteIds) { - return statementsToExecuteIds.contains(statementInfoWrapper.getId()); + synchronized (statementsToExecuteLabels) { + return statementsToExecuteLabels.contains(statementInfoWrapper.getLabel()); } } private void closeAllResults() { synchronized (this) { - if (this.firstUnclosedStatementResult != null) { - this.firstUnclosedStatementResult.close(); - this.firstUnclosedStatementResult = null; + if (firstUnclosedStatementResult != null) { + firstUnclosedStatementResult.close(); + firstUnclosedStatementResult = null; } } } @Override public void cancel() throws SQLException { - synchronized (statementsToExecuteIds) { - statementsToExecuteIds.clear(); + synchronized (statementsToExecuteLabels) { + statementsToExecuteLabels.clear(); } - String statementId = runningStatementId; - if (statementId != null) { - log.info("Cancelling statement with id " + statementId); - try { - statementService.abortStatementHttpRequest(statementId); - } finally { - abortStatementRunningOnFirebolt(statementId); - } + String statementLabel = runningStatementLabel; + if (statementLabel != null) { + log.log(Level.INFO, "Cancelling statement with label {0}", statementLabel); + abortStatementRunningOnFirebolt(statementLabel); } } - private void abortStatementRunningOnFirebolt(String statementId) throws SQLException { + private void abortStatementRunningOnFirebolt(String statementLabel) throws SQLException { try { - statementService.abortStatement(statementId, this.sessionProperties); - log.debug("Statement with id {} was aborted", statementId); - } catch (FireboltException e) { - throw e; + statementService.abortStatement(statementLabel, sessionProperties); + log.log(Level.FINE, "Statement with label {0} was aborted", statementLabel); } catch (Exception e) { throw new FireboltException("Could not abort statement", e); } finally { @@ -194,14 +186,14 @@ private void abortStatementRunningOnFirebolt(String statementId) throws SQLExcep @Override public int executeUpdate(String sql) throws SQLException { - return this.executeUpdate(StatementUtil.parseToStatementInfoWrappers(sql)); + return executeUpdate(StatementUtil.parseToStatementInfoWrappers(sql)); } protected int executeUpdate(List sql) throws SQLException { - this.execute(sql); + execute(sql); StatementResultWrapper response; synchronized (this) { - response = this.firstUnclosedStatementResult; + response = firstUnclosedStatementResult; } try { while (response != null && response.getResultSet() != null) { @@ -218,7 +210,7 @@ protected int executeUpdate(List sql) throws SQLException @Override public Connection getConnection() { - return this.connection; + return connection; } @Override @@ -226,20 +218,20 @@ public boolean getMoreResults(int current) throws SQLException { synchronized (this) { validateStatementIsNotClosed(); - if (current == Statement.CLOSE_CURRENT_RESULT && this.currentStatementResult != null - && this.currentStatementResult.getResultSet() != null) { - this.currentStatementResult.getResultSet().close(); + if (current == Statement.CLOSE_CURRENT_RESULT && currentStatementResult != null + && currentStatementResult.getResultSet() != null) { + currentStatementResult.getResultSet().close(); } - if (this.currentStatementResult != null) { - this.currentStatementResult = this.currentStatementResult.getNext(); + if (currentStatementResult != null) { + currentStatementResult = currentStatementResult.getNext(); } if (current == Statement.CLOSE_ALL_RESULTS) { closeUnclosedProcessedResults(); } - return (this.currentStatementResult != null && this.currentStatementResult.getResultSet() != null); + return (currentStatementResult != null && currentStatementResult.getResultSet() != null); } } @@ -255,14 +247,14 @@ private synchronized void closeUnclosedProcessedResults() throws SQLException { } @Override - public int getMaxRows() throws SQLException { + public int getMaxRows() { return maxRows; } @Override public void setMaxRows(int max) throws SQLException { if (max < 0) { - throw new FireboltException(String.format("Illegal maxRows value: %d", max)); + throw new FireboltException(format("Illegal maxRows value: %d", max)); } maxRows = max; } @@ -288,23 +280,23 @@ public void close(boolean removeFromConnection) throws SQLException { } isClosed = true; } - this.closeAllResults(); + closeAllResults(); - if (removeFromConnection) { + if (removeFromConnection && connection != null) { connection.removeClosedStatement(this); } cancel(); - log.debug("Statement closed"); + log.fine("Statement closed"); } @Override - public boolean isClosed() throws SQLException { - return this.isClosed; + public boolean isClosed() { + return isClosed; } @Override - public synchronized ResultSet getResultSet() throws SQLException { - return this.firstUnclosedStatementResult != null ? this.firstUnclosedStatementResult.getResultSet() : null; + public synchronized ResultSet getResultSet() { + return firstUnclosedStatementResult != null ? firstUnclosedStatementResult.getResultSet() : null; } @Override @@ -317,43 +309,30 @@ public boolean getMoreResults() throws SQLException { } @Override - public int getUpdateCount() throws SQLException { + public int getUpdateCount() { return currentUpdateCount; } @Override - public void closeOnCompletion() throws SQLException { + public void closeOnCompletion() { closeOnCompletion = true; } @Override - public boolean isCloseOnCompletion() throws SQLException { + public boolean isCloseOnCompletion() { return closeOnCompletion; } @Override - public int getQueryTimeout() throws SQLException { + public int getQueryTimeout() { return queryTimeout; } @Override - public void setQueryTimeout(int seconds) throws SQLException { + public void setQueryTimeout(int seconds) { queryTimeout = seconds; } - @Override - public boolean isWrapperFor(Class iface) { - return iface.isAssignableFrom(getClass()); - } - - @Override - public T unwrap(Class iface) throws SQLException { - if (iface.isAssignableFrom(getClass())) { - return iface.cast(this); - } - throw new SQLException("Cannot unwrap to " + iface.getName()); - } - protected void validateStatementIsNotClosed() throws SQLException { if (isClosed()) { throw new FireboltException("Cannot proceed: statement closed"); @@ -375,40 +354,24 @@ protected StatementInfoWrapper getOneQueryStatementInfo(List result = new ArrayList<>(); + for (String sql : batchStatements) { + for (StatementInfoWrapper query : StatementUtil.parseToStatementInfoWrappers(sql)) { + @SuppressWarnings("java:S6912") // Use "addBatch" and "executeBatch" to execute multiple SQL statements in a single call - this is the implementation of executeBatch + Optional rs = execute(List.of(query)); + result.add(rs.map(x -> 0).orElse(SUCCESS_NO_INFO)); + } + } + return result.stream().mapToInt(Integer::intValue).toArray(); } @Override @@ -482,50 +446,61 @@ public ResultSet getGeneratedKeys() throws SQLException { } @Override - @NotImplemented public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new FireboltSQLFeatureNotSupportedException(); + } + return executeUpdate(sql); } @Override - @NotImplemented public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (columnIndexes == null || columnIndexes.length == 0) { + return executeUpdate(sql); + } + throw new FireboltSQLFeatureNotSupportedException("Returning autogenerated keys by column index is not supported."); } @Override - @NotImplemented public int executeUpdate(String sql, String[] columnNames) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (columnNames == null || columnNames.length == 0) { + return executeUpdate(sql); + } + throw new FireboltSQLFeatureNotSupportedException("Returning autogenerated keys by column name is not supported."); } @Override - @NotImplemented public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new FireboltSQLFeatureNotSupportedException(); + } + return execute(sql); } @Override - @NotImplemented public boolean execute(String sql, int[] columnIndexes) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (columnIndexes == null || columnIndexes.length == 0) { + return execute(sql); + } + throw new FireboltSQLFeatureNotSupportedException("Returning autogenerated keys by column index is not supported."); } @Override - @NotImplemented public boolean execute(String sql, String[] columnNames) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + if (columnNames == null || columnNames.length == 0) { + return execute(sql); + } + throw new FireboltSQLFeatureNotSupportedException("Returning autogenerated keys by column name is not supported."); } @Override - @NotImplemented - public int getResultSetHoldability() throws SQLException { + public int getResultSetHoldability() { // N/A applicable as we do not support transactions => commits do not affect anything => kind of hold cursors over commit return ResultSet.HOLD_CURSORS_OVER_COMMIT; } @Override - public boolean isPoolable() throws SQLException { + public boolean isPoolable() { return false; } @@ -541,6 +516,6 @@ public void setPoolable(boolean poolable) throws SQLException { * @return true if the statement has more results */ public boolean hasMoreResults() { - return this.currentStatementResult.getNext() != null; + return currentStatementResult.getNext() != null; } } diff --git a/src/main/java/com/firebolt/jdbc/statement/ParamMarker.java b/src/main/java/com/firebolt/jdbc/statement/ParamMarker.java index 93b011a3..f3063583 100644 --- a/src/main/java/com/firebolt/jdbc/statement/ParamMarker.java +++ b/src/main/java/com/firebolt/jdbc/statement/ParamMarker.java @@ -6,6 +6,6 @@ @AllArgsConstructor @Value public class ParamMarker { - int id; // Id / index of the param marker in the SQL statement + int id; // ID / index of the param marker in the SQL statement int position; // Position in the SQL subStatement } \ No newline at end of file diff --git a/src/main/java/com/firebolt/jdbc/statement/StatementInfoWrapper.java b/src/main/java/com/firebolt/jdbc/statement/StatementInfoWrapper.java index 30482685..997fe6d8 100644 --- a/src/main/java/com/firebolt/jdbc/statement/StatementInfoWrapper.java +++ b/src/main/java/com/firebolt/jdbc/statement/StatementInfoWrapper.java @@ -1,53 +1,49 @@ package com.firebolt.jdbc.statement; -import static com.firebolt.jdbc.statement.StatementType.PARAM_SETTING; - -import java.util.UUID; - -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.statement.rawstatement.RawStatement; import com.firebolt.jdbc.statement.rawstatement.SetParamRawStatement; - import lombok.AllArgsConstructor; -import lombok.Data; +import lombok.Getter; import lombok.NonNull; +import java.util.Map.Entry; +import java.util.UUID; + +import static com.firebolt.jdbc.statement.StatementType.PARAM_SETTING; + /** * This represents a statement that is ready to be sent to Firebolt or executed * internally to set a param */ -@Data +@Getter @AllArgsConstructor public class StatementInfoWrapper { - private String sql; + private final String sql; + private final String label; private String id; - private StatementType type; - private Pair param; - private RawStatement initialStatement; - - /** - * Creates a StatementInfoWrapper from the {@link RawStatement}. - * - * @param rawStatement the raw statement - * @return the statement that will be sent to the server - */ - public static StatementInfoWrapper of(@NonNull RawStatement rawStatement) { - return of(rawStatement, UUID.randomUUID().toString()); + private final StatementType type; + private final Entry param; + private final RawStatement initialStatement; + + public StatementInfoWrapper(String sql, StatementType type, Entry param, RawStatement initialStatement) { + this.sql = sql; + this.type = type; + this.param = param; + this.initialStatement = initialStatement; + this.label = UUID.randomUUID().toString(); } /** * Creates a StatementInfoWrapper from the {@link RawStatement}. * * @param rawStatement the raw statement - * @param id the id of the statement to execute * @return the statement that will be sent to the server */ - public static StatementInfoWrapper of(@NonNull RawStatement rawStatement, String id) { - Pair additionalProperties = rawStatement.getStatementType() == PARAM_SETTING + public static StatementInfoWrapper of(@NonNull RawStatement rawStatement) { + Entry additionalProperties = rawStatement.getStatementType() == PARAM_SETTING ? ((SetParamRawStatement) rawStatement).getAdditionalProperty() : null; - return new StatementInfoWrapper(rawStatement.getSql(), id, rawStatement.getStatementType(), + return new StatementInfoWrapper(rawStatement.getSql(), rawStatement.getStatementType(), additionalProperties, rawStatement); } } diff --git a/src/main/java/com/firebolt/jdbc/statement/StatementResultWrapper.java b/src/main/java/com/firebolt/jdbc/statement/StatementResultWrapper.java index c8203132..a67cbd1d 100644 --- a/src/main/java/com/firebolt/jdbc/statement/StatementResultWrapper.java +++ b/src/main/java/com/firebolt/jdbc/statement/StatementResultWrapper.java @@ -1,16 +1,16 @@ package com.firebolt.jdbc.statement; -import java.io.Closeable; -import java.sql.ResultSet; +import lombok.Data; import javax.annotation.Nullable; - -import lombok.CustomLog; -import lombok.Data; +import java.io.Closeable; +import java.sql.ResultSet; +import java.util.logging.Level; +import java.util.logging.Logger; @Data -@CustomLog public class StatementResultWrapper implements Closeable { + private static final Logger log = Logger.getLogger(StatementResultWrapper.class.getName()); private ResultSet resultSet; private int updateCount; private StatementInfoWrapper statementInfoWrapper; @@ -29,7 +29,7 @@ public void close() { resultSet.close(); } } catch (Exception e) { - log.warn("Could not close ResultSet", e); + log.log(Level.WARNING, "Could not close ResultSet", e); } if (next != null) { next.close(); diff --git a/src/main/java/com/firebolt/jdbc/statement/StatementUtil.java b/src/main/java/com/firebolt/jdbc/statement/StatementUtil.java index 1d7e7078..256dfc1e 100644 --- a/src/main/java/com/firebolt/jdbc/statement/StatementUtil.java +++ b/src/main/java/com/firebolt/jdbc/statement/StatementUtil.java @@ -1,30 +1,32 @@ package com.firebolt.jdbc.statement; -import java.util.*; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.apache.commons.lang3.RegExUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.statement.rawstatement.RawStatement; import com.firebolt.jdbc.statement.rawstatement.RawStatementWrapper; import com.firebolt.jdbc.statement.rawstatement.SetParamRawStatement; - -import lombok.CustomLog; +import com.firebolt.jdbc.util.StringUtil; import lombok.NonNull; import lombok.experimental.UtilityClass; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + @UtilityClass -@CustomLog public class StatementUtil { private static final String SET_PREFIX = "set"; private static final Pattern SET_WITH_SPACE_REGEX = Pattern.compile(SET_PREFIX + " ", Pattern.CASE_INSENSITIVE); private static final String[] SELECT_KEYWORDS = new String[] { "show", "select", "describe", "exists", "explain", "with", "call" }; + private static final Logger log = Logger.getLogger(StatementUtil.class.getName()); /** * Returns true if the statement is a query (eg: SELECT, SHOW). @@ -33,12 +35,11 @@ public class StatementUtil { * @return true if the statement is a query (eg: SELECT, SHOW). */ public static boolean isQuery(String cleanSql) { - if (StringUtils.isNotEmpty(cleanSql)) { - cleanSql = cleanSql.replace("(", ""); - return StringUtils.startsWithAny(cleanSql.toLowerCase(), SELECT_KEYWORDS); - } else { + if (cleanSql == null || cleanSql.isEmpty()) { return false; } + String lowerCaseSql = cleanSql.replace("(", "").toLowerCase(); + return Arrays.stream(SELECT_KEYWORDS).anyMatch(lowerCaseSql::startsWith); } /** @@ -48,11 +49,8 @@ public static boolean isQuery(String cleanSql) { * @param sql the sql statement * @return an optional parameter represented with a pair of key/value */ - public Optional> extractParamFromSetStatement(@NonNull String cleanSql, String sql) { - if (StringUtils.startsWithIgnoreCase(cleanSql, SET_PREFIX)) { - return extractPropertyPair(cleanSql, sql); - } - return Optional.empty(); + public Optional> extractParamFromSetStatement(@NonNull String cleanSql, String sql) { + return cleanSql.toLowerCase().startsWith(SET_PREFIX) ? extractPropertyPair(cleanSql, sql) : Optional.empty(); } /** @@ -69,7 +67,7 @@ public List parseToStatementInfoWrappers(String sql) { /** * Parse sql statement to a {@link RawStatementWrapper}. The method construct * the {@link RawStatementWrapper} by splitting it in a list of sub-statements - * (supports multistatements) + * (supports multi-statements) * * @param sql the sql statement * @return a list of {@link StatementInfoWrapper} @@ -90,7 +88,7 @@ public RawStatementWrapper parseToRawStatementWrapper(String sql) { boolean isInSingleLineComment = false; boolean isInMultipleLinesComment = false; boolean isInComment = false; - boolean foundSubqueryEndingSemicolon = false; + boolean foundSubQueryEndingSemicolon = false; char previousChar; int subQueryParamsCount = 0; boolean isPreviousCharInComment; @@ -105,16 +103,16 @@ public RawStatementWrapper parseToRawStatementWrapper(String sql) { isInComment = isInSingleLineComment || isInMultipleLinesComment; if (!isInComment) { // Although the ending semicolon may have been found, we need to include any - // potential comments to the subquery + // potential comments to the sub-query if (!isCurrentSubstringBetweenQuotes && isEndingSemicolon(currentChar, previousChar, - foundSubqueryEndingSemicolon, isPreviousCharInComment)) { - foundSubqueryEndingSemicolon = true; - if (isEndOfSubquery(currentChar)) { + foundSubQueryEndingSemicolon, isPreviousCharInComment)) { + foundSubQueryEndingSemicolon = true; + if (isEndOfSubQuery(currentChar)) { subStatements.add(RawStatement.of(sql.substring(subQueryStart, currentIndex), subStatementParamMarkersPositions, cleanedSubQuery.toString().trim())); subStatementParamMarkersPositions = new ArrayList<>(); subQueryStart = currentIndex; - foundSubqueryEndingSemicolon = false; + foundSubQueryEndingSemicolon = false; cleanedSubQuery = new StringBuilder(); } } else if (currentChar == '?' && !isCurrentSubstringBetweenQuotes @@ -136,15 +134,15 @@ public RawStatementWrapper parseToRawStatementWrapper(String sql) { return new RawStatementWrapper(subStatements); } - private boolean isEndingSemicolon(char currentChar, char previousChar, boolean foundSubqueryEndingSemicolon, + private boolean isEndingSemicolon(char currentChar, char previousChar, boolean foundSubQueryEndingSemicolon, boolean isPreviousCharInComment) { - if (foundSubqueryEndingSemicolon) { + if (foundSubQueryEndingSemicolon) { return true; } return (';' == previousChar && currentChar != ';' && !isPreviousCharInComment); } - private boolean isEndOfSubquery(char currentChar) { + private boolean isEndOfSubQuery(char currentChar) { return currentChar != '-' && currentChar != '/' && currentChar != ' ' && currentChar != '\n'; } @@ -180,25 +178,26 @@ public Map getParamMarketsPositions(String sql) { * @param cleanSql the clean sql query * @return the database name and the table name from the sql query as a pair */ - public Pair, Optional> extractDbNameAndTableNamePairFromCleanQuery(String cleanSql) { + public Entry, Optional> extractDbNameAndTableNamePairFromCleanQuery(String cleanSql) { Optional from = Optional.empty(); if (isQuery(cleanSql)) { - log.debug("Extracting DB and Table name for SELECT: {}", cleanSql); - String withoutQuotes = StringUtils.replace(cleanSql, "'", "").trim(); - if (StringUtils.startsWithIgnoreCase(withoutQuotes, "select")) { - int fromIndex = StringUtils.indexOfIgnoreCase(withoutQuotes, "from"); + log.log(Level.FINE, "Extracting DB and Table name for SELECT: {0}", cleanSql); + String withoutQuotes = cleanSql.replace("'", "").trim(); + String withoutQuotesUpperCase = withoutQuotes.toUpperCase(); + if (withoutQuotesUpperCase.startsWith("SELECT")) { + int fromIndex = withoutQuotesUpperCase.indexOf("FROM"); if (fromIndex != -1) { - from = Optional.of(withoutQuotes.substring(fromIndex + "from".length()).trim().split(" ")[0]); + from = Optional.of(withoutQuotes.substring(fromIndex + "FROM".length()).trim().split(" ")[0]); } - } else if (StringUtils.startsWithIgnoreCase(withoutQuotes, "DESCRIBE")) { + } else if (withoutQuotesUpperCase.startsWith("DESCRIBE")) { from = Optional.of("tables"); - } else if (StringUtils.startsWithIgnoreCase(withoutQuotes, "SHOW")) { + } else if (withoutQuotesUpperCase.startsWith("SHOW")) { from = Optional.empty(); // Depends on the information requested } else { - log.debug("Could not find table name for query {}. This may happen when there is no table.", cleanSql); + log.log(Level.FINE, "Could not find table name for query {0}. This may happen when there is no table.", cleanSql); } } - return new ImmutablePair<>(extractDbNameFromFromPartOfTheQuery(from.orElse(null)), + return Map.entry(extractDbNameFromFromPartOfTheQuery(from.orElse(null)), extractTableNameFromFromPartOfTheQuery(from.orElse(null))); } @@ -227,14 +226,14 @@ public static List replaceParameterMarksWithValues(@NonNul public List replaceParameterMarksWithValues(@NonNull Map params, @NonNull RawStatementWrapper rawStatement) { List subQueries = new ArrayList<>(); - for (int subqueryIndex = 0; subqueryIndex < rawStatement.getSubStatements().size(); subqueryIndex++) { + for (int subQueryIndex = 0; subQueryIndex < rawStatement.getSubStatements().size(); subQueryIndex++) { int currentPos; /* * As the parameter markers are being placed then the statement sql keeps * getting bigger, which is why we need to keep track of the offset */ int offset = 0; - RawStatement subQuery = rawStatement.getSubStatements().get(subqueryIndex); + RawStatement subQuery = rawStatement.getSubStatements().get(subQueryIndex); String subQueryWithParams = subQuery.getSql(); if (params.size() != rawStatement.getTotalParams()) { @@ -255,31 +254,25 @@ public List replaceParameterMarksWithValues(@NonNull Map additionalParams = subQuery.getStatementType() == StatementType.PARAM_SETTING + Entry additionalParams = subQuery.getStatementType() == StatementType.PARAM_SETTING ? ((SetParamRawStatement) subQuery).getAdditionalProperty() : null; - subQueries.add(new StatementInfoWrapper(subQueryWithParams, UUID.randomUUID().toString(), - subQuery.getStatementType(), additionalParams, subQuery)); - + subQueries.add(new StatementInfoWrapper(subQueryWithParams, subQuery.getStatementType(), additionalParams, subQuery)); } return subQueries; } private Optional extractTableNameFromFromPartOfTheQuery(String from) { return Optional.ofNullable(from).map(s -> s.replace("\"", "")).map(fromPartOfTheQuery -> { - if (StringUtils.contains(fromPartOfTheQuery, ".")) { - int indexOfTableName = StringUtils.lastIndexOf(fromPartOfTheQuery, "."); - return fromPartOfTheQuery.substring(indexOfTableName + 1); - } else { - return fromPartOfTheQuery; - } + int indexOfTableName = fromPartOfTheQuery.lastIndexOf('.'); + return indexOfTableName >= 0 && indexOfTableName < fromPartOfTheQuery.length() - 1 ? fromPartOfTheQuery.substring(indexOfTableName + 1) : fromPartOfTheQuery; }); } private static Optional extractDbNameFromFromPartOfTheQuery(String from) { return Optional.ofNullable(from).map(s -> s.replace("\"", "")) - .filter(s -> StringUtils.countMatches(s, ".") == 2).map(fromPartOfTheQuery -> { - int dbNameEndPos = StringUtils.indexOf(fromPartOfTheQuery, "."); + .filter(s -> s.chars().filter(c -> '.' == c).count() == 2).map(fromPartOfTheQuery -> { + int dbNameEndPos = fromPartOfTheQuery.indexOf('.'); return fromPartOfTheQuery.substring(0, dbNameEndPos); }); } @@ -294,19 +287,14 @@ private boolean isInSingleLineComment(char currentChar, char previousChar, boole return isInSingleLineComment; } - private Optional> extractPropertyPair(String cleanStatement, String sql) { - String setQuery = RegExUtils.removeFirst(cleanStatement, SET_WITH_SPACE_REGEX); - String[] values = StringUtils.split(setQuery, "="); + private Optional> extractPropertyPair(String cleanStatement, String sql) { + String setQuery = SET_WITH_SPACE_REGEX.matcher(cleanStatement).replaceFirst(""); + String[] values = setQuery.split("="); if (values.length == 2) { - String value = StringUtils.removeEnd(values[1], ";").trim(); - if (StringUtils.isNumeric(value)){ - return Optional.of(Pair.of(values[0].trim(), value.trim())); - } else { - return Optional.of(Pair.of(values[0].trim(), StringUtils.removeEnd(StringUtils.removeStart(value, "'"), "'"))); - } - } else { - throw new IllegalArgumentException( - "Cannot parse the additional properties provided in the statement: " + sql); + String value = (values[1].endsWith(";") ? values[1].substring(0, values[1].length() - 1) : values[1]).trim(); + String pureValue = value.chars().allMatch(Character::isDigit) ? value : StringUtil.strip(value, '\''); + return Optional.of(Map.entry(values[0].trim(), pureValue)); } + throw new IllegalArgumentException("Cannot parse the additional properties provided in the statement: " + sql); } } diff --git a/src/main/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatement.java b/src/main/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatement.java index 15fb4c82..e7f20b89 100644 --- a/src/main/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatement.java +++ b/src/main/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatement.java @@ -3,6 +3,7 @@ import com.firebolt.jdbc.annotation.NotImplemented; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.exception.FireboltSQLFeatureNotSupportedException; import com.firebolt.jdbc.exception.FireboltUnsupportedOperationException; @@ -12,10 +13,10 @@ import com.firebolt.jdbc.statement.StatementUtil; import com.firebolt.jdbc.statement.rawstatement.RawStatementWrapper; import com.firebolt.jdbc.type.JavaTypeToFireboltSQLString; -import lombok.Builder; -import lombok.CustomLog; +import com.firebolt.jdbc.util.InputStreamUtil; import lombok.NonNull; +import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.math.BigDecimal; @@ -41,23 +42,33 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import static com.firebolt.jdbc.statement.StatementUtil.replaceParameterMarksWithValues; +import static com.firebolt.jdbc.statement.rawstatement.StatementValidatorFactory.createValidator; +import static java.lang.String.format; +import static java.sql.Types.DECIMAL; +import static java.sql.Types.NUMERIC; +import static java.sql.Types.VARBINARY; -@CustomLog public class FireboltPreparedStatement extends FireboltStatement implements PreparedStatement { - + private static final Logger log = Logger.getLogger(FireboltPreparedStatement.class.getName()); private final RawStatementWrapper rawStatement; private final List> rows; private Map providedParameters; - @Builder(builderMethodName = "statementBuilder") // As the parent is also using @Builder, a method name is mandatory + public FireboltPreparedStatement(FireboltStatementService statementService, FireboltConnection connection, String sql) { + this(statementService, connection.getSessionProperties(), connection, sql); + } + public FireboltPreparedStatement(FireboltStatementService statementService, FireboltProperties sessionProperties, - String sql, FireboltConnection connection) { + FireboltConnection connection, String sql) { super(statementService, sessionProperties, connection); - log.debug("Populating PreparedStatement object for SQL: {}", sql); + log.log(Level.FINE, "Populating PreparedStatement object for SQL: {0}", sql); this.providedParameters = new HashMap<>(); this.rawStatement = StatementUtil.parseToRawStatementWrapper(sql); + rawStatement.getSubStatements().forEach(statement -> createValidator(statement, connection).validate(statement)); this.rows = new ArrayList<>(); } @@ -68,94 +79,99 @@ public ResultSet executeQuery() throws SQLException { } private List prepareSQL(@NonNull Map params) { - return replaceParameterMarksWithValues(params, this.rawStatement); + return replaceParameterMarksWithValues(params, rawStatement); } @Override public int executeUpdate() throws SQLException { - this.validateStatementIsNotClosed(); - return super.executeUpdate(prepareSQL(this.providedParameters)); + validateStatementIsNotClosed(); + return super.executeUpdate(prepareSQL(providedParameters)); } @Override public void setNull(int parameterIndex, int sqlType) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.NULL_VALUE); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.NULL_VALUE); } @Override public void setBoolean(int parameterIndex, boolean x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.BOOLEAN.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.BOOLEAN.transform(x)); } @Override public void setByte(int parameterIndex, byte x) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.BYTE.transform(x)); } @Override public void setShort(int parameterIndex, short x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.SHORT.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.SHORT.transform(x)); } @Override public void setInt(int parameterIndex, int x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.INTEGER.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.INTEGER.transform(x)); } @Override public void setLong(int parameterIndex, long x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.LONG.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.LONG.transform(x)); } @Override public void setFloat(int parameterIndex, float x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.FLOAT.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.FLOAT.transform(x)); } @Override public void setDouble(int parameterIndex, double x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.DOUBLE.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.DOUBLE.transform(x)); } @Override public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.BIG_DECIMAL.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.BIG_DECIMAL.transform(x)); } @Override public void setString(int parameterIndex, String x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.STRING.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.STRING.transform(x)); } @Override - @NotImplemented - public void setBytes(int parameterIndex, byte[] x) throws SQLException { - throw new SQLFeatureNotSupportedException("The format Byte is currently not supported"); + public void setBytes(int parameterIndex, byte[] bytes) throws SQLException { + if (bytes == null) { + setNull(parameterIndex, VARBINARY); + } else { + setObject(parameterIndex, bytes, VARBINARY); + } } @Override public void setDate(int parameterIndex, Date x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.DATE.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.DATE.transform(x)); } @Override @@ -166,86 +182,92 @@ public void setTime(int parameterIndex, Time x) throws SQLException { @Override public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.TIMESTAMP.transform(x)); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.TIMESTAMP.transform(x)); } @Override - public void clearParameters() throws SQLException { - this.providedParameters.clear(); - this.rows.clear(); + public void clearParameters() { + providedParameters.clear(); + rows.clear(); } @Override public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.setObject(parameterIndex, x); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + try { + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.transformAny(x, targetSqlType)); + } catch (FireboltException fbe) { + if (ExceptionType.TYPE_NOT_SUPPORTED.equals(fbe.getType())) { + throw new SQLFeatureNotSupportedException(fbe.getMessage(), fbe); + } + throw fbe; + } } @Override public void setObject(int parameterIndex, Object x) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.transformAny(x)); } @Override public boolean execute() throws SQLException { - this.validateStatementIsNotClosed(); + validateStatementIsNotClosed(); return super.execute(prepareSQL(providedParameters)).isPresent(); } @Override - public void addBatch() throws SQLException { - rows.add(this.providedParameters); - this.providedParameters = new HashMap<>(); + public void addBatch() { + rows.add(providedParameters); + providedParameters = new HashMap<>(); } @Override public ResultSetMetaData getMetaData() throws SQLException { - ResultSet resultSet = this.getResultSet(); + ResultSet resultSet = getResultSet(); return resultSet != null ? resultSet.getMetaData() : null; } @Override public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { - this.validateStatementIsNotClosed(); - this.validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.NULL_VALUE); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.NULL_VALUE); } @Override - @NotImplemented - public void setURL(int parameterIndex, URL x) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + public void setURL(int parameterIndex, URL url) throws SQLException { + setString(parameterIndex, url == null ? null : url.toString()); } @Override public void setNString(int parameterIndex, String value) throws SQLException { - this.validateStatementIsNotClosed(); + validateStatementIsNotClosed(); validateParamIndex(parameterIndex); - this.setString(parameterIndex, value); + setString(parameterIndex, value); } @Override public void setArray(int parameterIndex, Array x) throws SQLException { - this.validateStatementIsNotClosed(); + validateStatementIsNotClosed(); validateParamIndex(parameterIndex); - this.providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.ARRAY.transform(x)); + providedParameters.put(parameterIndex, JavaTypeToFireboltSQLString.ARRAY.transform(x)); } @Override public int[] executeBatch() throws SQLException { - this.validateStatementIsNotClosed(); - log.debug("Executing batch for statement: {}", rawStatement); + validateStatementIsNotClosed(); + log.log(Level.FINE, "Executing batch for statement: {0}", rawStatement); List inserts = new ArrayList<>(); - int[] result = new int[this.rows.size()]; + int[] result = new int[rows.size()]; for (Map row : rows) { - inserts.addAll(this.prepareSQL(row)); + inserts.addAll(prepareSQL(row)); } - this.execute(inserts); + execute(inserts); for (int i = 0; i < inserts.size(); i++) { result[i] = SUCCESS_NO_INFO; } @@ -258,10 +280,10 @@ public int executeUpdate(String sql) throws SQLException { throw new FireboltException("Cannot call method executeUpdate(String sql) on a PreparedStatement"); } - private void validateParamIndex(int paramIndex) throws FireboltException { - if (this.rawStatement.getTotalParams() < paramIndex) { + private void validateParamIndex(int paramIndex) throws SQLException { + if (rawStatement.getTotalParams() < paramIndex) { throw new FireboltException( - String.format("Cannot set parameter as there is no parameter at index: %d for statement: %s", + format("Cannot set parameter as there is no parameter at index: %d for statement: %s", paramIndex, rawStatement)); } } @@ -272,9 +294,12 @@ public boolean execute(String sql) throws SQLException { } @Override - @NotImplemented public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + try { + setString(parameterIndex, reader == null ? null : InputStreamUtil.read(reader, length)); + } catch (IOException e) { + throw new SQLException(e); + } } @Override @@ -284,21 +309,18 @@ public void setRef(int parameterIndex, Ref x) throws SQLException { } @Override - @NotImplemented - public void setBlob(int parameterIndex, Blob x) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + public void setBlob(int parameterIndex, Blob blob) throws SQLException { + setBytes(parameterIndex, blob == null ? null : blob.getBytes(1, (int)blob.length())); } @Override - @NotImplemented - public void setClob(int parameterIndex, Clob x) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + public void setClob(int parameterIndex, Clob clob) throws SQLException { + setString(parameterIndex, clob == null ? null : clob.getSubString(1, (int)clob.length())); } @Override - @NotImplemented - public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { - throw new FireboltUnsupportedOperationException(); + public void setDate(int parameterIndex, Date date, Calendar calendar) throws SQLException { + setDateTime(parameterIndex, date, calendar, JavaTypeToFireboltSQLString.DATE); } @Override @@ -308,9 +330,18 @@ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLExceptio } @Override - @NotImplemented - public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { - throw new FireboltUnsupportedOperationException(); + public void setTimestamp(int parameterIndex, Timestamp timestamp, Calendar calendar) throws SQLException { + setDateTime(parameterIndex, timestamp, calendar, JavaTypeToFireboltSQLString.TIMESTAMP); + } + + private void setDateTime(int parameterIndex, T datetime, Calendar calendar, JavaTypeToFireboltSQLString type) throws SQLException { + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + if (datetime == null || calendar == null) { + providedParameters.put(parameterIndex, type.transform(datetime)); + } else { + providedParameters.put(parameterIndex, type.transform(datetime, calendar.getTimeZone().getID())); + } } @Override @@ -326,33 +357,28 @@ public void setRowId(int parameterIndex, RowId x) throws SQLException { } @Override - @NotImplemented public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setCharacterStream(parameterIndex, value, length); } @Override - @NotImplemented public void setNClob(int parameterIndex, NClob value) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setClob(parameterIndex, value); } @Override - @NotImplemented public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); - } + setCharacterStream(parameterIndex, reader, length); + } @Override - @NotImplemented public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setBinaryStream(parameterIndex, inputStream, length); } @Override - @NotImplemented public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setClob(parameterIndex, reader, length); } @Override @@ -362,86 +388,96 @@ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException } @Override - @NotImplemented public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + validateStatementIsNotClosed(); + validateParamIndex(parameterIndex); + try { + // scaleOfLength should affect only DECIMAL and NUMERIC types + boolean isNumber = (DECIMAL == targetSqlType || NUMERIC == targetSqlType) && x instanceof Number; + String str = isNumber ? formatDecimalNumber(x, scaleOrLength) : JavaTypeToFireboltSQLString.transformAny(x, targetSqlType); + providedParameters.put(parameterIndex, str); + } catch (FireboltException fbe) { + if (ExceptionType.TYPE_NOT_SUPPORTED.equals(fbe.getType())) { + throw new SQLFeatureNotSupportedException(fbe.getMessage(), fbe); + } + } + } + + private String formatDecimalNumber(Object x, int scaleOrLength) { + String format = format("%%.%df", scaleOrLength); + return format(format, ((Number)x).doubleValue()); } @Override - @NotImplemented public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { - throw new FireboltUnsupportedOperationException(); + setBinaryStream(parameterIndex, x, length); } @Override - @NotImplemented - public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + public void setBinaryStream(int parameterIndex, InputStream inputStream, long length) throws SQLException { + setBinaryStream(parameterIndex, inputStream, (int)length); } @Override - @NotImplemented public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { - throw new FireboltUnsupportedOperationException(); + setCharacterStream(parameterIndex, reader, (int)length); } @Override - @NotImplemented public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { - throw new FireboltUnsupportedOperationException(); + setBinaryStream(parameterIndex, x); } @Override - @NotImplemented public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { - throw new FireboltUnsupportedOperationException(); + try { + setBytes(parameterIndex, x == null ? null : x.readAllBytes()); + } catch (IOException e) { + throw new SQLException(e); + } } @Override - @NotImplemented public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setCharacterStream(parameterIndex, reader, Integer.MAX_VALUE); } @Override - @NotImplemented public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setCharacterStream(parameterIndex, value); } @Override - @NotImplemented public void setClob(int parameterIndex, Reader reader) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setClob(parameterIndex, reader, Integer.MAX_VALUE); } @Override - @NotImplemented public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); - } + setBinaryStream(parameterIndex, inputStream); + } @Override - @NotImplemented public void setNClob(int parameterIndex, Reader reader) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setClob(parameterIndex, reader); } @Override - @NotImplemented public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { - throw new FireboltUnsupportedOperationException(); + setBinaryStream(parameterIndex, x, length); } @Override - @NotImplemented public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { - throw new FireboltSQLFeatureNotSupportedException(); + setBinaryStream(parameterIndex, x, length); } @Override - @NotImplemented - public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { - throw new FireboltUnsupportedOperationException(); + public void setBinaryStream(int parameterIndex, InputStream inputStream, int length) throws SQLException { + try { + setBytes(parameterIndex, inputStream == null ? null : inputStream.readNBytes(length)); + } catch (IOException e) { + throw new SQLException(e); + } } } diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/NoOpStatementValidator.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/NoOpStatementValidator.java new file mode 100644 index 00000000..fdaddb50 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/NoOpStatementValidator.java @@ -0,0 +1,8 @@ +package com.firebolt.jdbc.statement.rawstatement; + +public class NoOpStatementValidator implements StatementValidator { + @Override + public void validate(RawStatement statement) { + // do nothing + } +} diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/QueryRawStatement.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/QueryRawStatement.java index a0cea063..2456edb3 100644 --- a/src/main/java/com/firebolt/jdbc/statement/rawstatement/QueryRawStatement.java +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/QueryRawStatement.java @@ -1,22 +1,20 @@ package com.firebolt.jdbc.statement.rawstatement; -import static com.firebolt.jdbc.statement.StatementType.QUERY; - -import java.util.List; -import java.util.Optional; - -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.statement.ParamMarker; import com.firebolt.jdbc.statement.StatementType; import com.firebolt.jdbc.statement.StatementUtil; - import lombok.EqualsAndHashCode; import lombok.Getter; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; + +import static com.firebolt.jdbc.statement.StatementType.QUERY; + /** * A query statement is a statement that returns data (Typically starts with - * SELECT, SHOW, etc) + * SELECT, SHOW, etc.) */ @Getter @EqualsAndHashCode(callSuper = true) @@ -28,10 +26,10 @@ public class QueryRawStatement extends RawStatement { public QueryRawStatement(String sql, String cleanSql, List paramPositions) { super(sql, cleanSql, paramPositions); - Pair, Optional> databaseAndTablePair = StatementUtil + Entry, Optional> databaseAndTablePair = StatementUtil .extractDbNameAndTableNamePairFromCleanQuery(this.getCleanSql()); - this.database = databaseAndTablePair.getLeft().orElse(null); - this.table = databaseAndTablePair.getRight().orElse(null); + this.database = databaseAndTablePair.getKey().orElse(null); + this.table = databaseAndTablePair.getValue().orElse(null); } @Override diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatement.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatement.java index 2f126edb..7268f02e 100644 --- a/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatement.java +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatement.java @@ -1,17 +1,15 @@ package com.firebolt.jdbc.statement.rawstatement; -import java.util.List; -import java.util.Optional; - -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.statement.ParamMarker; import com.firebolt.jdbc.statement.StatementType; import com.firebolt.jdbc.statement.StatementUtil; - import lombok.Data; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.stream.Collectors; + @Data public abstract class RawStatement { @@ -26,7 +24,7 @@ protected RawStatement(String sql, String cleanSql, List paramPosit } public static RawStatement of(String sql, List paramPositions, String cleanSql) { - Optional> additionalProperties = StatementUtil.extractParamFromSetStatement(cleanSql, sql); + Optional> additionalProperties = StatementUtil.extractParamFromSetStatement(cleanSql, sql); if (additionalProperties.isPresent()) { return new SetParamRawStatement(sql, cleanSql, paramPositions, additionalProperties.get()); } else if (StatementUtil.isQuery(cleanSql)) { @@ -39,7 +37,7 @@ public static RawStatement of(String sql, List paramPositions, Stri @Override public String toString() { return "RawSqlStatement{" + "sql='" + sql + '\'' + ", cleanSql='" + cleanSql + '\'' + ", paramMarkers=" - + StringUtils.join(paramMarkers, "|") + '}'; + + paramMarkers.stream().map(ParamMarker::toString).collect(Collectors.joining("|")) + '}'; } public List getParamMarkers() { diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatementWrapper.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatementWrapper.java index db4f1fe8..75e967df 100644 --- a/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatementWrapper.java +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/RawStatementWrapper.java @@ -1,14 +1,11 @@ package com.firebolt.jdbc.statement.rawstatement; +import lombok.Value; + import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; -import org.apache.commons.lang3.StringUtils; - -import lombok.CustomLog; -import lombok.Value; - -@CustomLog @Value public class RawStatementWrapper { @@ -23,7 +20,7 @@ public RawStatementWrapper(List subStatements) { @Override public String toString() { - return "SqlQueryWrapper{" + "subQueries=" + StringUtils.join(subStatements, "|") + ", totalParams=" + return "SqlQueryWrapper{" + "subQueries=" + subStatements.stream().map(RawStatement::toString).collect(Collectors.joining("|")) + ", totalParams=" + totalParams + '}'; } diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetParamRawStatement.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetParamRawStatement.java index 89bf0c5b..87fe4500 100644 --- a/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetParamRawStatement.java +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetParamRawStatement.java @@ -1,17 +1,15 @@ package com.firebolt.jdbc.statement.rawstatement; -import static com.firebolt.jdbc.statement.StatementType.PARAM_SETTING; - -import java.util.List; - -import org.apache.commons.lang3.tuple.Pair; - import com.firebolt.jdbc.statement.ParamMarker; import com.firebolt.jdbc.statement.StatementType; - import lombok.EqualsAndHashCode; import lombok.Getter; +import java.util.List; +import java.util.Map.Entry; + +import static com.firebolt.jdbc.statement.StatementType.PARAM_SETTING; + /** * A Set param statement is a special statement that sets a parameter internally * (this type of statement starts with SET) @@ -19,11 +17,9 @@ @Getter @EqualsAndHashCode(callSuper = true) public class SetParamRawStatement extends RawStatement { + private final Entry additionalProperty; - private final Pair additionalProperty; - - public SetParamRawStatement(String sql, String cleanSql, List paramPositions, - Pair additionalProperty) { + public SetParamRawStatement(String sql, String cleanSql, List paramPositions, Entry additionalProperty) { super(sql, cleanSql, paramPositions); this.additionalProperty = additionalProperty; } @@ -32,5 +28,4 @@ public SetParamRawStatement(String sql, String cleanSql, List param public StatementType getStatementType() { return PARAM_SETTING; } - } diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetValidator.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetValidator.java new file mode 100644 index 00000000..4552aee0 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/SetValidator.java @@ -0,0 +1,49 @@ +package com.firebolt.jdbc.statement.rawstatement; + +import com.firebolt.jdbc.connection.FireboltConnection; + +import java.util.Arrays; +import java.util.Map; +import java.util.TreeMap; + +import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.ACCOUNT_ID; +import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.DATABASE; +import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.ENGINE; +import static com.firebolt.jdbc.connection.settings.FireboltQueryParameterKey.OUTPUT_FORMAT; +import static java.lang.String.CASE_INSENSITIVE_ORDER; +import static java.lang.String.format; +import static java.util.stream.Collectors.toMap; + +public class SetValidator implements StatementValidator { + private static final Map forbiddenParameters1 = caseInsensitiveNameSet(DATABASE, ENGINE, ACCOUNT_ID, OUTPUT_FORMAT); + private static final Map forbiddenParameters2 = caseInsensitiveNameSet(DATABASE, ENGINE, OUTPUT_FORMAT); + private static final Map useSupporting = caseInsensitiveNameSet(DATABASE, ENGINE); + private static final String FORBIDDEN_PROPERTY_ERROR_PREFIX = "Could not set parameter. Set parameter '%s' is not allowed. "; + private static final String FORBIDDEN_PROPERTY_ERROR_USE_SUFFIX = "Try again with 'USE %s' instead of SET."; + private static final String FORBIDDEN_PROPERTY_ERROR_SET_SUFFIX = "Try again with a different parameter name."; + private static final String USE_ERROR = FORBIDDEN_PROPERTY_ERROR_PREFIX + FORBIDDEN_PROPERTY_ERROR_USE_SUFFIX; + private static final String SET_ERROR = FORBIDDEN_PROPERTY_ERROR_PREFIX + FORBIDDEN_PROPERTY_ERROR_SET_SUFFIX; + + private final Map forbiddenParameters; + + public SetValidator(FireboltConnection connection) { + forbiddenParameters = connection.getInfraVersion() < 2 ? forbiddenParameters1 : forbiddenParameters2; + } + + @Override + public void validate(RawStatement statement) { + validateProperty(((SetParamRawStatement)statement).getAdditionalProperty().getKey()); + } + + private void validateProperty(String name) { + String standardName = forbiddenParameters.get(name); + if (standardName != null) { + throw new IllegalArgumentException(format(useSupporting.containsKey(name) ? USE_ERROR : SET_ERROR, standardName, standardName)); + } + } + + @SafeVarargs + private static > Map caseInsensitiveNameSet(Enum ... elements) { + return Arrays.stream(elements).map(Enum::name).collect(toMap(name -> name, name -> name, (one, two) -> two, () -> new TreeMap<>(CASE_INSENSITIVE_ORDER))); + } +} diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidator.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidator.java new file mode 100644 index 00000000..219b3fbf --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidator.java @@ -0,0 +1,5 @@ +package com.firebolt.jdbc.statement.rawstatement; + +public interface StatementValidator { + void validate(RawStatement statement); +} diff --git a/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidatorFactory.java b/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidatorFactory.java new file mode 100644 index 00000000..2e26d0ae --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/statement/rawstatement/StatementValidatorFactory.java @@ -0,0 +1,13 @@ +package com.firebolt.jdbc.statement.rawstatement; + +import com.firebolt.jdbc.connection.FireboltConnection; + +public abstract class StatementValidatorFactory { + private StatementValidatorFactory() { + // empty private constructor to ensure that this class will be used as factory only. + } + + public static StatementValidator createValidator(RawStatement statement, FireboltConnection connection) { + return statement instanceof SetParamRawStatement ? new SetValidator(connection) : new NoOpStatementValidator(); + } +} diff --git a/src/main/java/com/firebolt/jdbc/type/BaseType.java b/src/main/java/com/firebolt/jdbc/type/BaseType.java index 742c4c91..b46c2b09 100644 --- a/src/main/java/com/firebolt/jdbc/type/BaseType.java +++ b/src/main/java/com/firebolt/jdbc/type/BaseType.java @@ -6,12 +6,10 @@ import com.firebolt.jdbc.type.array.SqlArrayUtil; import com.firebolt.jdbc.type.date.SqlDateUtil; import lombok.Builder; -import lombok.CustomLog; import lombok.Value; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; -import javax.xml.bind.DatatypeConverter; +import javax.annotation.Nonnull; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Array; @@ -19,16 +17,27 @@ import java.sql.SQLException; import java.sql.Time; import java.sql.Timestamp; +import java.util.Arrays; import java.util.TimeZone; +import java.util.function.Predicate; +import java.util.regex.Pattern; -/** This class contains the java types the Firebolt datatypes are mapped to */ -@CustomLog +import static com.firebolt.jdbc.exception.ExceptionType.TYPE_TRANSFORMATION_ERROR; +import static com.firebolt.jdbc.type.array.SqlArrayUtil.BYTE_ARRAY_PREFIX; +import static com.firebolt.jdbc.type.array.SqlArrayUtil.hexStringToByteArray; + +/** This class contains the java types the Firebolt data types are mapped to */ public enum BaseType { - LONG(Long.class, conversion -> Long.parseLong(checkInfinity(conversion.getValue()))), - INTEGER(Integer.class, conversion -> Integer.parseInt(checkInfinity(conversion.getValue()))), - SHORT(Short.class, conversion -> Short.parseShort(checkInfinity(conversion.getValue()))), - BIGINT(BigInteger.class, conversion -> new BigInteger(checkInfinity(conversion.getValue()))), - TEXT(String.class, conversion -> StringEscapeUtils.unescapeJava(conversion.getValue())), + LONG(TypePredicate.mayBeFloatingNumber, Long.class, conversion -> Long.parseLong(checkInfinity(conversion.getValue())), conversion -> Double.valueOf(conversion.getValue()).longValue()), + INTEGER(TypePredicate.mayBeFloatingNumber, Integer.class, conversion -> Integer.parseInt(checkInfinity(conversion.getValue())), conversion -> Integer.parseInt(Long.toString(Double.valueOf(conversion.getValue()).longValue()))), + SHORT(TypePredicate.mayBeFloatingNumber, Short.class, conversion -> Short.parseShort(checkInfinity(conversion.getValue())), conversion -> Short.parseShort(Long.toString(Double.valueOf(conversion.getValue()).longValue()))), + BYTE(TypePredicate.mayBeFloatingNumber, Byte.class, conversion -> Byte.parseByte(checkInfinity(conversion.getValue())), conversion -> Byte.parseByte(Long.toString(Double.valueOf(conversion.getValue()).longValue()))), + BIGINT(TypePredicate.mayBeFloatingNumber, BigInteger.class, conversion -> new BigInteger(checkInfinity(conversion.getValue())), conversion -> BigInteger.valueOf(Double.valueOf(conversion.getValue()).longValue())), + TEXT(String.class, conversion -> { + String escaped = StringEscapeUtils.unescapeJava(conversion.getValue()); + int limit = conversion.getMaxFieldSize(); + return limit > 0 && limit <= escaped.length() ? escaped.substring(0, limit) : escaped; + }), REAL(Float.class, conversion -> { if (isNan(conversion.getValue())) { return Float.NaN; @@ -64,11 +73,12 @@ public enum BaseType { OBJECT(Object.class, StringToColumnTypeConversion::getValue), NUMERIC(BigDecimal.class, conversion -> new BigDecimal(conversion.getValue())), BOOLEAN(Boolean.class, conversion -> { - if (StringUtils.equalsAnyIgnoreCase(conversion.getValue(), "0", "f")) { - return false; - } else if (StringUtils.equalsAnyIgnoreCase(conversion.getValue(), "1", "t")) { - return true; - } + String value = conversion.getValue(); + if ("0".equals(value) || "f".equalsIgnoreCase(value)) { + return false; + } else if ("1".equals(value) || "t".equalsIgnoreCase(value)) { + return true; + } throw new FireboltException(String.format("Cannot cast %s to type boolean", conversion.getValue())); }), ARRAY(Array.class, conversion -> SqlArrayUtil.transformToSqlArray(conversion.getValue(), conversion.getColumn().getType())), @@ -77,28 +87,42 @@ public enum BaseType { if (s == null || s.isEmpty()) { return new byte[] {}; } - if (s.startsWith("\\x")) { - return DatatypeConverter.parseHexBinary(s.substring(2)); + if (s.startsWith(BYTE_ARRAY_PREFIX)) { + byte[] bytes = hexStringToByteArray(s); + int limit = conversion.getMaxFieldSize(); + return limit > 0 && limit <= bytes.length ? Arrays.copyOf(bytes, limit) : bytes; } // Cannot convert from other formats (such as 'Escape') for the moment throw new FireboltException("Cannot convert binary string in non-hex format to byte array"); }); + // this class is needed to prevent back reference because the constant is used from the enum constructor + private static final class TypePredicate { + private static final Predicate mayBeFloatingNumber = Pattern.compile("[.eE]").asPredicate(); + } public static final String NULL_VALUE = "\\N"; private final Class type; - private final CheckedFunction transformFunction; + private final Predicate shouldTryFallback; + private final CheckedFunction[] transformFunctions; - BaseType(Class type, CheckedFunction transformFunction) { + @SafeVarargs + BaseType(Class type, CheckedFunction... transformFunctions) { + this(s -> true, type, transformFunctions); + } + + @SafeVarargs + BaseType(Predicate shouldTryFallback, Class type, CheckedFunction... transformFunctions) { this.type = type; - this.transformFunction = transformFunction; + this.shouldTryFallback = shouldTryFallback; + this.transformFunctions = transformFunctions; } private static boolean isPositiveInf(String value) { - return StringUtils.equalsAnyIgnoreCase(value, "+inf", "inf"); + return "inf".equalsIgnoreCase(value) || "+inf".equalsIgnoreCase(value); } private static boolean isNegativeInf(String value) { - return StringUtils.equals(value, "-inf"); + return "-inf".equalsIgnoreCase(value); } private static String checkInfinity(String s) { @@ -109,11 +133,11 @@ private static String checkInfinity(String s) { } public static boolean isNull(String value) { - return StringUtils.equalsIgnoreCase(value, NULL_VALUE); + return NULL_VALUE.equalsIgnoreCase(value); } private static boolean isNan(String value) { - return StringUtils.equalsIgnoreCase(value, "nan"); + return "nan".equalsIgnoreCase(value) || "+nan".equalsIgnoreCase(value) || "-nan".equalsIgnoreCase(value); } private static void validateObjectNotNull(String value) { @@ -127,14 +151,14 @@ public Class getType() { } public T transform(String value, Column column) throws SQLException { - return this.transform(value, column, null); + return transform(value, column, null, 0); } public T transform(String value) throws SQLException { - return this.transform(value, null, null); + return transform(value, null, null, 0); } - public T transform(String value, Column column, TimeZone timeZone) throws SQLException { + public T transform(@Nonnull String value, Column column, TimeZone timeZone, int maxFieldSize) throws SQLException { TimeZone fromTimeZone; if (column != null && column.getType().getTimeZone() != null) { fromTimeZone = column.getType().getTimeZone(); @@ -142,8 +166,8 @@ public T transform(String value, Column column, TimeZone timeZone) throws SQ fromTimeZone = timeZone; } StringToColumnTypeConversion conversion = StringToColumnTypeConversion.builder().value(value).column(column) - .timeZone(fromTimeZone).build(); - return this.transform(conversion); + .timeZone(fromTimeZone).maxFieldSize(maxFieldSize).build(); + return transform(conversion); } private T transform(StringToColumnTypeConversion conversion) throws SQLException { @@ -151,7 +175,18 @@ private T transform(StringToColumnTypeConversion conversion) throws SQLExcep if (isNull(conversion.getValue())) { return null; } - return (T) transformFunction.apply(conversion); + for (int i = 0; i < transformFunctions.length; i++) { + try { + //noinspection unchecked + return (T) transformFunctions[i].apply(conversion); + } catch (RuntimeException e) { + if (i == transformFunctions.length - 1 || !shouldTryFallback.test(conversion.getValue())) { + throw new FireboltException(e.getMessage(), e, TYPE_TRANSFORMATION_ERROR); + } + } + } + // this can happen only if transformationFunctions is empty that is wrong, but we must satisfy the compiler. + throw new IllegalStateException(); } @Builder @@ -160,5 +195,6 @@ private static class StringToColumnTypeConversion { String value; Column column; TimeZone timeZone; + int maxFieldSize; } } diff --git a/src/main/java/com/firebolt/jdbc/type/FireboltDataType.java b/src/main/java/com/firebolt/jdbc/type/FireboltDataType.java index f2d4b36d..e1a9b630 100644 --- a/src/main/java/com/firebolt/jdbc/type/FireboltDataType.java +++ b/src/main/java/com/firebolt/jdbc/type/FireboltDataType.java @@ -34,7 +34,7 @@ public enum FireboltDataType { DATE_32(Types.DATE, FireboltDataTypeDisplayNames.PGDATE, BaseType.DATE, false, false, 10, 0, 0, true,"Date32", "PGDate"), DATE_TIME_64(Types.TIMESTAMP, FireboltDataTypeDisplayNames.TIMESTAMP, BaseType.TIMESTAMP, false, false, 19, 0, 6, true, "DateTime64", "TIMESTAMP"), TIMESTAMP(Types.TIMESTAMP, FireboltDataTypeDisplayNames.TIMESTAMP, BaseType.TIMESTAMP, false, false, 6, 0, 0, true,"DateTime", "TIMESTAMP", "TimestampNtz"), - TIMESTAMP_WITH_TIMEZONE(Types.TIMESTAMP_WITH_TIMEZONE, FireboltDataTypeDisplayNames.TIMESTAMPTZ, BaseType.TIMESTAMP_WITH_TIMEZONE, false, false, 6, 0, 0, true,"Timestamptz"), + TIMESTAMP_WITH_TIMEZONE(Types.TIMESTAMP_WITH_TIMEZONE, FireboltDataTypeDisplayNames.TIMESTAMPTZ, BaseType.TIMESTAMP_WITH_TIMEZONE, false, false, 6, 0, 0, true,"TimestampTz"), NOTHING(Types.NULL, FireboltDataTypeDisplayNames.NULL, BaseType.NULL, false, false, 0, 0, 0, false,"Nothing", "NULL"), UNKNOWN(Types.OTHER, FireboltDataTypeDisplayNames.UNKNOWN, BaseType.OTHER, false, false, 0, 0, 0, false,"Unknown"), NUMERIC(Types.NUMERIC, FireboltDataTypeDisplayNames.NUMERIC, BaseType.NUMERIC, true, false, 38, 0, 37, false, "Decimal", "DEC", "NUMERIC"), diff --git a/src/main/java/com/firebolt/jdbc/type/FireboltDataTypeDisplayNames.java b/src/main/java/com/firebolt/jdbc/type/FireboltDataTypeDisplayNames.java index c063c5f5..a404d02b 100644 --- a/src/main/java/com/firebolt/jdbc/type/FireboltDataTypeDisplayNames.java +++ b/src/main/java/com/firebolt/jdbc/type/FireboltDataTypeDisplayNames.java @@ -2,6 +2,7 @@ import lombok.experimental.UtilityClass; +@SuppressWarnings("SpellCheckingInspection") @UtilityClass public class FireboltDataTypeDisplayNames { static final String INTEGER = "integer"; diff --git a/src/main/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLString.java b/src/main/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLString.java index 963139fe..2229f622 100644 --- a/src/main/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLString.java +++ b/src/main/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLString.java @@ -1,88 +1,171 @@ package com.firebolt.jdbc.type; +import com.firebolt.jdbc.CheckedBiFunction; import com.firebolt.jdbc.CheckedFunction; +import com.firebolt.jdbc.CheckedSupplier; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.type.array.SqlArrayUtil; import com.firebolt.jdbc.type.date.SqlDateUtil; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import java.math.BigDecimal; import java.math.BigInteger; import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Date; +import java.sql.JDBCType; +import java.sql.SQLException; import java.sql.Timestamp; -import java.util.Arrays; -import java.util.UUID; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.TimeZone; +import java.util.stream.Stream; import static com.firebolt.jdbc.exception.ExceptionType.TYPE_NOT_SUPPORTED; import static com.firebolt.jdbc.exception.ExceptionType.TYPE_TRANSFORMATION_ERROR; +import static com.firebolt.jdbc.type.array.SqlArrayUtil.byteArrayToHexString; +import static java.lang.String.format; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.toMap; public enum JavaTypeToFireboltSQLString { BOOLEAN(Boolean.class, value -> Boolean.TRUE.equals(value) ? "1" : "0"), - UUID(java.util.UUID.class, value -> ((UUID) value).toString()), - SHORT(Short.class, value -> Short.toString((short) value)), + UUID(java.util.UUID.class, Object::toString), + BYTE(Byte.class, value -> Byte.toString(((Number) value).byteValue())), + SHORT(Short.class, value -> Short.toString(((Number) value).shortValue())), STRING(String.class, getSQLStringValueOfString()), - LONG(Long.class, String::valueOf), - INTEGER(Integer.class, String::valueOf), - BIG_INTEGER(BigInteger.class, String::valueOf), - FLOAT(Float.class, String::valueOf), - DOUBLE(Double.class, String::valueOf), - DATE(Date.class, date -> SqlDateUtil.transformFromDateToSQLStringFunction.apply((Date) date)), - TIMESTAMP(Timestamp.class, time -> SqlDateUtil.transformFromTimestampToSQLStringFunction.apply((Timestamp) time)), + LONG(Long.class, value -> Long.toString(((Number)value).longValue())), + INTEGER(Integer.class, value -> Integer.toString(((Number)value).intValue())), + BIG_INTEGER(BigInteger.class, value -> value instanceof BigInteger ? value.toString() : Long.toString(((Number)value).longValue())), + FLOAT(Float.class, value -> Float.toString(((Number)value).floatValue())), + DOUBLE(Double.class, value -> Double.toString(((Number)value).doubleValue())), + DATE(Date.class, date -> SqlDateUtil.transformFromDateToSQLStringFunction.apply((Date) date), (date, tz) -> SqlDateUtil.transformFromDateWithTimezoneToSQLStringFunction.apply((Date) date, toTimeZone(tz))), + TIMESTAMP(Timestamp.class, time -> SqlDateUtil.transformFromTimestampToSQLStringFunction.apply((Timestamp) time), (ts, tz) -> SqlDateUtil.transformFromTimestampWithTimezoneToSQLStringFunction.apply((Timestamp) ts, toTimeZone(tz))), BIG_DECIMAL(BigDecimal.class, value -> value == null ? BaseType.NULL_VALUE : ((BigDecimal) value).toPlainString()), - ARRAY(Array.class, SqlArrayUtil::arrayToString); + ARRAY(Array.class, SqlArrayUtil::arrayToString), + BYTE_ARRAY(byte[].class, value -> ofNullable(byteArrayToHexString((byte[])value, true)).map(x -> format("E'%s'::BYTEA", x)).orElse(null)), + ; + private static final List> characterToEscapedCharacterPairs = List.of( + Map.entry("\0", "\\0"), Map.entry("\\", "\\\\"), Map.entry("'", "''")); + //https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html + private static final Map> jdbcTypeToClass = Map.ofEntries( + Map.entry(JDBCType.CHAR, String.class), + Map.entry(JDBCType.VARCHAR, String.class), + Map.entry(JDBCType.LONGVARCHAR,String.class), + Map.entry(JDBCType.NUMERIC, java.math.BigDecimal.class), + Map.entry(JDBCType.DECIMAL, java.math.BigDecimal.class), + Map.entry(JDBCType.BIT, Boolean.class), + Map.entry(JDBCType.BOOLEAN, Boolean.class), + Map.entry(JDBCType.TINYINT, Short.class), + Map.entry(JDBCType.SMALLINT, Short.class), + Map.entry(JDBCType.INTEGER, Integer.class), + Map.entry(JDBCType.BIGINT, Long.class), + Map.entry(JDBCType.REAL, Float.class), + Map.entry(JDBCType.FLOAT, Double.class), + Map.entry(JDBCType.DOUBLE, Double.class), + Map.entry(JDBCType.BINARY, byte[].class), + Map.entry(JDBCType.VARBINARY, byte[].class), + Map.entry(JDBCType.LONGVARBINARY, byte[].class), + Map.entry(JDBCType.DATE, java.sql.Date.class), + Map.entry(JDBCType.TIME, java.sql.Time.class), + Map.entry(JDBCType.TIMESTAMP, java.sql.Timestamp.class), + //DISTINCT Object type of underlying type + Map.entry(JDBCType.CLOB, Clob.class), + Map.entry(JDBCType.BLOB, Blob.class), + Map.entry(JDBCType.ARRAY, Array.class) + //STRUCT, Struct or SQLData + //Map.entry(JDBCType.REF, Ref.class) + //Map.entry(JDBCType.JAVA_OBJECT, Object.class) + ); - private static final Pair characterToEscapedCharacterPair = new ImmutablePair<>( - new String[] { "\0", "\\", "'" }, new String[] { "\\0", "\\\\", "\\'" }); private final Class sourceType; private final CheckedFunction transformToJavaTypeFunction; + private final CheckedBiFunction transformToJavaTypeFunctionWithParameter; public static final String NULL_VALUE = "NULL"; + private static final Map, JavaTypeToFireboltSQLString> classToType = Stream.of(JavaTypeToFireboltSQLString.values()) + .collect(toMap(type -> type.sourceType, type -> type)); JavaTypeToFireboltSQLString(Class sourceType, CheckedFunction transformToSqlStringFunction) { + this(sourceType, transformToSqlStringFunction, null); + } + + JavaTypeToFireboltSQLString(Class sourceType, + CheckedFunction transformToSqlStringFunction, + CheckedBiFunction transformToJavaTypeFunctionWithParameter) { this.sourceType = sourceType; this.transformToJavaTypeFunction = transformToSqlStringFunction; + this.transformToJavaTypeFunctionWithParameter = transformToJavaTypeFunctionWithParameter; } - public static String transformAny(Object object) throws FireboltException { - Class objectType; - if (object == null) { - return NULL_VALUE; - } else if (object.getClass().isArray()) { - objectType = Array.class; - } else { - objectType = object.getClass(); - } - JavaTypeToFireboltSQLString converter = Arrays.stream(JavaTypeToFireboltSQLString.values()) - .filter(c -> c.getSourceType().equals(objectType)).findAny() + public static String transformAny(Object object) throws SQLException { + return transformAny(object, () -> getType(object)); + } + + public static String transformAny(Object object, int sqlType) throws SQLException { + return transformAny(object, () -> getType(sqlType)); + } + + private static String transformAny(Object object, CheckedSupplier> classSupplier) throws SQLException { + return object == null ? NULL_VALUE : transformAny(object, classSupplier.get()); + } + + private static String transformAny(Object object, Class objectType) throws SQLException { + JavaTypeToFireboltSQLString converter = Optional.ofNullable(classToType.get(objectType)) .orElseThrow(() -> new FireboltException( - String.format("Cannot convert type %s. The type is not supported.", objectType), + format("Cannot convert type %s. The type is not supported.", objectType), TYPE_NOT_SUPPORTED)); return converter.transform(object); } + private static Class getType(Object object) { + return object.getClass().isArray() && !byte[].class.equals(object.getClass()) ? Array.class : object.getClass(); + } + + private static Class getType(int sqlType) throws SQLException { + try { + JDBCType jdbcType = JDBCType.valueOf(sqlType); + return Optional.ofNullable(jdbcTypeToClass.get(jdbcType)) + .orElseThrow(() -> new FireboltException(format("Unsupported JDBC type %s", jdbcType), TYPE_NOT_SUPPORTED)); + } catch(IllegalArgumentException e) { + throw new FireboltException(format("Unsupported SQL type %d", sqlType), TYPE_NOT_SUPPORTED); + } + } + private static CheckedFunction getSQLStringValueOfString() { return value -> { - String escaped = StringUtils.replaceEach((String) value, characterToEscapedCharacterPair.getLeft(), - characterToEscapedCharacterPair.getRight()); - return String.format("'%s'", escaped); + String escaped = (String) value; + for (Entry specialCharacter : characterToEscapedCharacterPairs) { + escaped = escaped.replace(specialCharacter.getKey(), specialCharacter.getValue()); + } + return format("'%s'", escaped); }; } - public Class getSourceType() { - return sourceType; - } - - public String transform(Object object) throws FireboltException { + public String transform(Object object, Object ... more) throws SQLException { if (object == null) { return NULL_VALUE; } else { try { - return this.transformToJavaTypeFunction.apply(object); + if (more.length > 0) { + return transformToJavaTypeFunctionWithParameter.apply(object, more[0]); + } + return transformToJavaTypeFunction.apply(object); } catch (Exception e) { throw new FireboltException("Could not convert object to a String ", e, TYPE_TRANSFORMATION_ERROR); } } } + + @SuppressWarnings("java:S6201") // Pattern Matching for "instanceof" was introduced in java 16 while we still try to be compliant with java 11 + private static TimeZone toTimeZone(Object tz) { + if (tz instanceof TimeZone) { + return (TimeZone)tz; + } + if (tz instanceof String) { + return TimeZone.getTimeZone((String)tz); + } + throw new IllegalArgumentException(format("Cannot convert %s to TimeZone", tz)); + } } diff --git a/src/main/java/com/firebolt/jdbc/type/array/SqlArrayUtil.java b/src/main/java/com/firebolt/jdbc/type/array/SqlArrayUtil.java index 6e50e597..03f1b01e 100644 --- a/src/main/java/com/firebolt/jdbc/type/array/SqlArrayUtil.java +++ b/src/main/java/com/firebolt/jdbc/type/array/SqlArrayUtil.java @@ -1,86 +1,131 @@ package com.firebolt.jdbc.type.array; -import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.resultset.column.ColumnType; import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.type.JavaTypeToFireboltSQLString; -import lombok.CustomLog; +import com.firebolt.jdbc.util.StringUtil; import lombok.NonNull; -import lombok.experimental.UtilityClass; -import org.apache.commons.lang3.StringUtils; +import javax.annotation.Nullable; import java.lang.reflect.Array; +import java.nio.ByteBuffer; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Optional.ofNullable; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; -@UtilityClass -@CustomLog public class SqlArrayUtil { + private static final Map formatMarkers = Map.of( + '[', new Markers('[', ']', '\'', '\''), + '{', new Markers('{', '}', '"', '\'') + ); + public static final String BYTE_ARRAY_PREFIX = "\\x"; + private static final Logger log = Logger.getLogger(SqlArrayUtil.class.getName()); + private final ColumnType columnType; + private final Markers markers; + + private static final class Markers { + private final char leftArrayBracket; + private final char rightArrayBracket; + private final char literalQuote; + private final char tupleLiteralQuote; + + public Markers(char leftArrayBracket, char rightArrayBracket, char literalQuote, char tupleLiteralQuote) { + this.leftArrayBracket = leftArrayBracket; + this.rightArrayBracket = rightArrayBracket; + this.literalQuote = literalQuote; + this.tupleLiteralQuote = tupleLiteralQuote; + } + } + + private SqlArrayUtil(ColumnType columnType, Markers markers) { + this.columnType = columnType; + this.markers = markers; + } - public static FireboltArray transformToSqlArray(String value, ColumnType columnType) throws SQLException { - log.debug("Transformer array with value {} and type {}", value, columnType); + public static FireboltArray transformToSqlArray(String value, ColumnType columnType) throws SQLException { + log.log(Level.FINE, "Transformer array with value {0} and type {1}", new Object[] {value, columnType}); + if (isNullValue(value)) { + return null; + } + int dimensions = getDimensions(columnType); + SqlArrayUtil parser = new SqlArrayUtil(columnType, + ofNullable(formatMarkers.get(value.charAt(0))).orElseThrow(() -> new IllegalArgumentException("Wrong format")) + ); + Object arr = parser.createArray(value, dimensions); + return arr == null ? null : new FireboltArray(columnType.getArrayBaseColumnType().getDataType(), arr); + } + + private static int getDimensions(ColumnType columnType) { int dimensions = 0; - for (int x = 0; x < value.length(); x++) - if (value.charAt(x) == '[') - dimensions++; - else - break; - value = value.substring(dimensions, value.length() - dimensions); - Object arr = createArray(value, dimensions, columnType); - return new FireboltArray(columnType.getArrayBaseColumnType().getDataType(), arr); - } - - private static Object createArray(String arrayContent, int dimension, ColumnType columnType) throws SQLException { - if (dimension == 1) { - return extractArrayFromOneDimensionalArray(arrayContent, columnType); - } else { - return extractArrayFromMultiDimensionalArray(arrayContent, dimension, columnType); + for (ColumnType type = columnType; FireboltDataType.ARRAY.equals(type.getDataType()); type = type.getInnerTypes().get(0)) { + dimensions++; } + return dimensions; + } + + private Object createArray(String arrayContent, int dimension) throws SQLException { + int from = arrayContent.charAt(0) == markers.leftArrayBracket ? 1 : 0; + int to = arrayContent.charAt(arrayContent.length() - 1) == markers.rightArrayBracket ? arrayContent.length() - 1 : arrayContent.length(); + arrayContent = arrayContent.substring(from, to); + return extractArray(arrayContent, dimension); + } + + private Object extractArray(String arrayContent, int dimension) throws SQLException { + if (isNullValue(arrayContent)) { + return null; + } + return dimension < 2 ? extractArrayFromOneDimensionalArray(arrayContent) : extractArrayFromMultiDimensionalArray(arrayContent, dimension); } @NonNull - private static Object extractArrayFromMultiDimensionalArray(String str, int dimension, ColumnType columnType) - throws SQLException { - String[] s = str.split(getArraySeparator(dimension)); + private Object extractArrayFromMultiDimensionalArray(String str, int dimension) throws SQLException { + String[] s = splitToElements(str); int[] lengths = new int[dimension]; lengths[0] = s.length; - Object currentArray = Array - .newInstance(columnType.getArrayBaseColumnType().getDataType().getBaseType().getType(), lengths); - - for (int x = 0; x < s.length; x++) - Array.set(currentArray, x, createArray(s[x], dimension - 1, columnType)); - + Object currentArray = Array.newInstance(columnType.getArrayBaseColumnType().getDataType().getBaseType().getType(), lengths); + for (int i = 0; i < s.length; i++) { + Array.set(currentArray, i, createArray(s[i], dimension - 1)); + } return currentArray; } - private static Object extractArrayFromOneDimensionalArray(String arrayContent, ColumnType columnType) - throws SQLException { - List elements = splitArrayContent(arrayContent, columnType.getArrayBaseColumnType().getDataType()) - .stream().filter(StringUtils::isNotEmpty).map(SqlArrayUtil::removeQuotesAndTransformNull) - .collect(Collectors.toList()); + private Object extractArrayFromOneDimensionalArray(String arrayContent) throws SQLException { FireboltDataType arrayBaseType = columnType.getArrayBaseColumnType().getDataType(); - if (arrayBaseType != FireboltDataType.TUPLE) { - Object currentArray = Array.newInstance(arrayBaseType.getBaseType().getType(), elements.size()); - for (int i = 0; i < elements.size(); i++) - Array.set(currentArray, i, arrayBaseType.getBaseType().transform(elements.get(i), null)); - return currentArray; - } else { + @SuppressWarnings("java:S6204") // JDK 11 compatible + List elements = splitArrayContent(arrayContent, arrayBaseType, markers.literalQuote) + .stream().filter(s -> s != null && !s.isEmpty()).map(x -> removeQuotesAndTransformNull(x, markers.literalQuote)) + .collect(toList()); + if (arrayBaseType == FireboltDataType.TUPLE) { return getArrayOfTuples(columnType, elements); } + Object currentArray = Array.newInstance(arrayBaseType.getBaseType().getType(), elements.size()); + for (int i = 0; i < elements.size(); i++) { + Array.set(currentArray, i, arrayBaseType.getBaseType().transform(elements.get(i), null)); + } + return currentArray; } - private static Object[] getArrayOfTuples(ColumnType columnType, List tuples) throws SQLException { + private Object[] getArrayOfTuples(ColumnType columnType, List tuples) throws SQLException { + @SuppressWarnings("java:S6204") // JDK 11 compatible List types = columnType.getArrayBaseColumnType().getInnerTypes().stream() - .map(ColumnType::getDataType).collect(Collectors.toList()); + .map(ColumnType::getDataType).collect(toList()); List list = new ArrayList<>(); for (String tupleContent : tuples) { List subList = new ArrayList<>(); - List tupleValues = splitArrayContent(removeParenthesis(tupleContent), FireboltDataType.TEXT); + List tupleValues = splitArrayContent(removeParenthesis(tupleContent), FireboltDataType.TEXT, markers.tupleLiteralQuote); for (int j = 0; j < types.size(); j++) { - subList.add(types.get(j).getBaseType().transform(removeQuotesAndTransformNull(tupleValues.get(j)))); + subList.add(types.get(j).getBaseType().transform(removeQuotesAndTransformNull(tupleValues.get(j), markers.tupleLiteralQuote))); } list.add(subList.toArray()); } @@ -89,33 +134,68 @@ private static Object[] getArrayOfTuples(ColumnType columnType, List tup return array; } - private static String getArraySeparator(int dimension) { - StringBuilder stringBuilder = new StringBuilder(","); - for (int x = 1; x < dimension; x++) { - stringBuilder.insert(0, ']'); - stringBuilder.append("\\["); + private String[] splitToElements(String value) { + char[] chars = value.toCharArray(); + int nesting = 0; + boolean intoString = false; + boolean escaped = false; + int from = 0; + List elements = new ArrayList<>(); + + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + if (c == '\\') { + escaped = true; + continue; + } + if (c == '\'' && !escaped) { + intoString = !intoString; + } + if (!intoString) { + if (c == markers.leftArrayBracket) { + nesting++; + } else if (c == markers.rightArrayBracket) { + nesting--; + } else if (c == ',') { + if (nesting <= 0) { + elements.add(value.substring(from, i)); + from = i + 1; + } + } + } + escaped = false; } - return stringBuilder.toString(); + elements.add(value.substring(from)); + return elements.toArray(new String[0]); } - private static String removeQuotesAndTransformNull(String s) { - return "NULL".equals(s) ? "\\N" : StringUtils.strip(s, "'"); + private String removeQuotesAndTransformNull(String s, char quote) { + return isNullValue(s) ? "\\N" : StringUtil.strip(s, quote); + } + + private static boolean isNullValue(String s) { + return "NULL".equals(s); } private static String removeParenthesis(String s) { return s.substring(1, s.length() - 1); } - private static List splitArrayContent(String arrayContent, FireboltDataType baseType) { + private static List splitArrayContent(String arrayContent, FireboltDataType baseType, char quote) { int index = -1; int subStringStart = 0; int parenthesisDepth = 0; // Needed for tuples boolean isCurrentSubstringBetweenQuotes = false; List elements = new ArrayList<>(); + boolean escaped = false; while (index < arrayContent.length() - 1) { index++; char currentChar = arrayContent.charAt(index); - if (currentChar == 39) { + if (currentChar == '\\') { + escaped = true; + continue; + } + if (currentChar == quote && !escaped) { isCurrentSubstringBetweenQuotes = !isCurrentSubstringBetweenQuotes; } if (!isCurrentSubstringBetweenQuotes && baseType == FireboltDataType.TUPLE) { @@ -129,6 +209,7 @@ private static List splitArrayContent(String arrayContent, FireboltDataT elements.add(arrayContent.substring(subStringStart, index)); subStringStart = index + 1; } + escaped = false; } elements.add(arrayContent.substring(subStringStart)); return elements; @@ -151,7 +232,7 @@ public static String arrayToString(Object o) throws SQLException { return toString(arr); } - private static String toString(Object[] arr) throws FireboltException { + private static String toString(Object[] arr) throws SQLException { if (arr == null) { return null; } @@ -168,8 +249,51 @@ private static Object[] toObjectArray(Object array) { } int length = Array.getLength(array); Object[] ret = new Object[length]; - for (int i = 0; i < length; i++) + for (int i = 0; i < length; i++) { ret[i] = Array.get(array, i); + } return ret; } + + /** + * Creates string representation of given byte array as a sequence of hexadecimal 2 character digits prepended + * by special marker {@code \x}. The same marker can be optionally used as a separator between hexadecimal digits + * depending on value of {@code separateEachByte}. + * + * @param bytes - the given byte array + * @param separateEachByte - flag that controls separator between hexadecimal digits in the resulting string + * @return hexadecimal representation of given array + */ + public static String byteArrayToHexString(@Nullable byte[] bytes, boolean separateEachByte) { + if (bytes == null) { + return null; + } + ByteBuffer buffer = ByteBuffer.wrap(bytes); + String separator = separateEachByte ? BYTE_ARRAY_PREFIX : ""; + return Stream.generate(buffer::get).limit(buffer.capacity()).map(i -> format("%02x", i)).collect(joining(separator, BYTE_ARRAY_PREFIX, "")); + } + + @SuppressWarnings("java:S1168") // we have to return null here + public static byte[] hexStringToByteArray(String str) { + if (str == null) { + return null; + } + if (!str.startsWith(BYTE_ARRAY_PREFIX)) { + return str.getBytes(UTF_8); + } + char[] chars = str.substring(2).toCharArray(); + byte[] bytes = new byte[chars.length / 2]; + for (int i = 0; i < chars.length; i += 2) { + bytes[i / 2] = (byte) ((hexDigit(chars[i]) << 4) + hexDigit(chars[i + 1])); + } + return bytes; + } + + private static int hexDigit(char c) { + int d = Character.digit(c, 16); + if (d < 0) { + throw new IllegalArgumentException(format("Illegal character %s in hex string", c)); + } + return d; + } } diff --git a/src/main/java/com/firebolt/jdbc/type/date/SqlDateUtil.java b/src/main/java/com/firebolt/jdbc/type/date/SqlDateUtil.java index cc60a344..d6d8b134 100644 --- a/src/main/java/com/firebolt/jdbc/type/date/SqlDateUtil.java +++ b/src/main/java/com/firebolt/jdbc/type/date/SqlDateUtil.java @@ -1,22 +1,21 @@ package com.firebolt.jdbc.type.date; +import com.firebolt.jdbc.CheckedBiFunction; +import lombok.experimental.UtilityClass; + import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.Instant; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; import java.util.TimeZone; +import java.util.function.BiFunction; import java.util.function.Function; -import com.firebolt.jdbc.CheckedBiFunction; - -import lombok.CustomLog; -import lombok.experimental.UtilityClass; - @UtilityClass -@CustomLog public class SqlDateUtil { public static final long ONE_DAY_MILLIS = 86400000L; @@ -28,12 +27,16 @@ public class SqlDateUtil { public static final Function transformFromTimestampToSQLStringFunction = value -> String .format("'%s'", dateTimeFormatter.format(value.toLocalDateTime())); + public static final BiFunction transformFromTimestampWithTimezoneToSQLStringFunction = (ts, tz) -> String + .format("'%s'", dateTimeFormatter.format(ts.toInstant().atZone(tz.toZoneId()).toLocalDateTime())); private static final TimeZone DEFAULT_SERVER_TZ = TimeZone.getTimeZone("UTC"); private static final DateTimeFormatter dateFormatter = new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4).parseDefaulting(ChronoField.YEAR, 0).appendPattern("[-]MM-dd") .toFormatter(); public static final Function transformFromDateToSQLStringFunction = value -> String.format("'%s'", dateFormatter.format(value.toLocalDate())); + public static final BiFunction transformFromDateWithTimezoneToSQLStringFunction = (date, tz) -> String.format("'%s'", + dateFormatter.format(Instant.ofEpochMilli(date.getTime()).atZone(tz.toZoneId()).toLocalDateTime())); public static final CheckedBiFunction transformToTimestampFunction = TimestampUtil::toTimestamp; public static final Function transformFromTimestampToOffsetDateTime = timestamp -> { diff --git a/src/main/java/com/firebolt/jdbc/type/lob/FireboltBlob.java b/src/main/java/com/firebolt/jdbc/type/lob/FireboltBlob.java new file mode 100644 index 00000000..83b27524 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/type/lob/FireboltBlob.java @@ -0,0 +1,83 @@ +package com.firebolt.jdbc.type.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.sql.Blob; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedList; + +public class FireboltBlob extends FireboltLob implements Blob { + public FireboltBlob() { + this(new byte[0]); + } + + public FireboltBlob(byte[] buf) { + super(buf, (a, i) -> a[i], Integer::byteValue, n -> (byte[])Array.newInstance(byte.class, n)); + } + + @Override + public byte[] getBytes(long pos, int length) throws SQLException { + isValid(buf); + validateGetRange(pos, length, buf.length); + int from = (int)pos - 1; + byte[] bytes = new byte[length]; + System.arraycopy(buf, from, bytes, 0, bytes.length); + return bytes; + } + + @Override + public InputStream getBinaryStream() throws SQLException { + isValid(buf); + return new ByteArrayInputStream(buf); + } + + @Override + public long position(byte[] pattern, long start) throws SQLException { + return super.position(pattern, start); + } + + @Override + public long position(Blob pattern, long start) throws SQLException { + return position(pattern.getBytes(1, (int)(pattern.length())), start); + } + + @Override + public int setBytes(long pos, byte[] bytes) throws SQLException { + return setBytes(pos, bytes, 0, bytes.length); + } + + @Override + public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { + return setData(pos, bytes, offset, len); + } + + @Override + public OutputStream setBinaryStream(long pos) throws SQLException { + return setStream(pos, new LinkedList<>()); + } + + @Override + public void truncate(long length) throws SQLException { + isValid(buf); + buf = length == 0 ? new byte[0] : getBytes(1, (int)length); + } + + @Override + public InputStream getBinaryStream(long pos, long length) throws SQLException { + return new ByteArrayInputStream(getBytes(pos, (int)length)); + } + + @Override + @SuppressWarnings("java:S6201") // Pattern Matching for "instanceof" was introduced in java 16 while we still try to be compliant with java 11 + public boolean equals(Object obj) { + return this == obj || (obj instanceof FireboltBlob && Arrays.equals(buf, ((FireboltBlob)obj).buf)); + } + + @Override + public int hashCode() { + return 31 * FireboltBlob.class.hashCode() + Arrays.hashCode(buf); + } +} diff --git a/src/main/java/com/firebolt/jdbc/type/lob/FireboltClob.java b/src/main/java/com/firebolt/jdbc/type/lob/FireboltClob.java new file mode 100644 index 00000000..c310afc2 --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/type/lob/FireboltClob.java @@ -0,0 +1,101 @@ +package com.firebolt.jdbc.type.lob; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.Writer; +import java.lang.reflect.Array; +import java.sql.Clob; +import java.sql.NClob; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedList; + +public class FireboltClob extends FireboltLob implements NClob { + public FireboltClob() { + this(new char[0]); + } + + public FireboltClob(char[] buf) { + super(buf, (a, i) -> a[i], i -> (char)i.intValue(), n -> (char[])Array.newInstance(char.class, n)); + } + + @Override + public String getSubString(long pos, int length) throws SQLException { + isValid(buf); + validateGetRange(pos, length, buf.length); + int from = (int)(pos - 1); + return new String(buf, from, Math.min(buf.length - from, length)); + } + + @Override + public Reader getCharacterStream() throws SQLException { + isValid(buf); + return new StringReader(new String(buf)); + } + + @Override + public InputStream getAsciiStream() throws SQLException { + isValid(buf); + return new ByteArrayInputStream(new String(buf).getBytes()); + } + + @Override + public long position(String searchStr, long start) throws SQLException { + return position(searchStr.toCharArray(), start); + } + + @Override + public long position(Clob searchStr, long start) throws SQLException { + return position(searchStr.getSubString(1, (int)searchStr.length()), start); + } + + @Override + public int setString(long pos, String str) throws SQLException { + return setString(pos, str, 0, str.length()); + } + + @Override + public int setString(long pos, String str, int offset, int len) throws SQLException { + return setChars(pos, str.toCharArray(), offset, len); + } + + private int setChars(long pos, char[] chars, int offset, int len) throws SQLException { + return setData(pos, chars, offset, len); + } + + @Override + public OutputStream setAsciiStream(long pos) throws SQLException { + return setStream(pos, new LinkedList<>()); + } + + @Override + public Writer setCharacterStream(long pos) throws SQLException { + return new OutputStreamWriter(setAsciiStream(pos)); + } + + @Override + public void truncate(long length) throws SQLException { + isValid(buf); + buf = length == 0 ? new char[0] : getSubString(1, (int)length).toCharArray(); + } + + @Override + public Reader getCharacterStream(long pos, long length) throws SQLException { + return new StringReader(getSubString(pos, (int)length)); + } + + @Override + @SuppressWarnings("java:S6201") // Pattern Matching for "instanceof" was introduced in java 16 while we still try to be compliant with java 11 + public boolean equals(Object obj) { + return this == obj || (obj instanceof FireboltClob && Arrays.equals(buf, ((FireboltClob)obj).buf)); + } + + @Override + public int hashCode() { + return 31 * FireboltClob.class.hashCode() + Arrays.hashCode(buf); + } +} diff --git a/src/main/java/com/firebolt/jdbc/type/lob/FireboltLob.java b/src/main/java/com/firebolt/jdbc/type/lob/FireboltLob.java new file mode 100644 index 00000000..7df29a0e --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/type/lob/FireboltLob.java @@ -0,0 +1,148 @@ +package com.firebolt.jdbc.type.lob; + +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Array; +import java.sql.SQLException; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Base class for implementations of Blob and Clob. It operates with primitive arrays (byte[] for Blob and char[] for Clob) + * and is optimized for better performance using lower level APIs like {@link System#arraycopy(Object, int, Object, int, int)} + * and minimizes primitive-to-wrapper and back conversions. This is the reason that this class has 2 generic parameters: + * the array of primitives {@code T} and the element type {@link E}. + */ +abstract class FireboltLob { + private final BiFunction elementGetter; + private final Function castor; + private final Function bufferFactory; + protected T buf; + + protected FireboltLob(T buf, BiFunction elementGetter, Function castor, Function bufferFactory) { + this.buf = buf; + this.elementGetter = elementGetter; + this.castor = castor; + this.bufferFactory = bufferFactory; + } + + + protected void validateGetRange(long pos, int length, int bufferLength) throws SQLException { + int from = (int)(pos - 1); + if (from < 0 || from > bufferLength) { + throw new SQLException("Invalid position in Clob object set"); + } + if (length < 0 || from + length > bufferLength) { + throw new SQLException("Invalid position and substring length"); + } + } + + protected void isValid(Object buf) throws SQLException { + if (buf == null) { + throw new SQLException("Error: You cannot call a method on a Blob instance once free() has been called."); + } + } + + protected void validateSetRange(long pos, int fragmentLength, int offset, int len) throws SQLException { + if (offset < 0 || offset + len > fragmentLength) { + throw new SQLException("Invalid offset in byte array set"); + } + if (pos < 1) { + throw new SQLException("Invalid position in Clob object set"); + } + } + + public long length() throws SQLException { + isValid(buf); + return Array.getLength(buf); + } + + @SuppressWarnings("SuspiciousSystemArraycopy") // guaranteed by subclass + protected int setData(long pos, T data, int offset, int len) throws SQLException { + isValid(buf); + validateSetRange(pos, Array.getLength(data), offset, len); + int index = (int)(pos - 1); + int bufLength = Array.getLength(buf); + int newLength = Math.max(bufLength, index + len); + @SuppressWarnings("unchecked") + T buffer = (T)Array.newInstance(buf.getClass().getComponentType(), newLength); + System.arraycopy(buf, 0, buffer, 0, bufLength); + System.arraycopy(data, offset, buffer, index, len); + buf = buffer; + return len; + } + + public void free() throws SQLException { + isValid(buf); + buf = null; + } + + @SuppressWarnings({"StatementWithEmptyBody", "java:S3776", "java:S127"}) + protected long position(T pattern, long start) throws SQLException { + isValid(buf); + int bufLength = Array.getLength(buf); + if (start < 1 || start > bufLength || bufLength == 0) { + return -1; + } + int patternLength = Array.getLength(pattern); + if (patternLength == 0) { + return 1; + } + int fromIndex = (int)(start - 1L); + int max = bufLength - patternLength; + for (int i = fromIndex; i <= max; i++) { + if (elementGetter.apply(buf, i) != elementGetter.apply(pattern, 0)) { + for (i++; i < max && elementGetter.apply(buf, i) != elementGetter.apply(pattern, 0); i++); + } + if (i <= max) { + int j = i + 1; + int end = j + patternLength - 1; + for (int k = 1; j < end && elementGetter.apply(buf, j) == elementGetter.apply(pattern, k); j++, k++); + if (j == end) { + return i + 1L; + } + } + } + return -1; + } + + protected OutputStream setStream(long pos, List temp) throws SQLException { + isValid(buf); + return new OutputStream() { + private int from = (int)(pos - 1); + private volatile boolean closed = false; + + @Override + public void write(int b) throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + temp.add(castor.apply(b)); + } + + @Override + @SuppressWarnings("SuspiciousSystemArraycopy") // guaranteed by subclass + public void flush() { + int length = temp.size(); + int bufLength = Array.getLength(buf); + int newLength = Math.max(bufLength, length + from); + if (newLength > bufLength) { + T newBuf = bufferFactory.apply(newLength); + System.arraycopy(buf, 0, newBuf, 0, bufLength); + buf = newBuf; + } + for (E b : temp) { + Array.set(buf, from++, b); + } + temp.clear(); + } + + @Override + public void close() { + flush(); + closed = true; + } + }; + } +} diff --git a/src/main/java/com/firebolt/jdbc/util/CloseableUtil.java b/src/main/java/com/firebolt/jdbc/util/CloseableUtil.java index 66057664..c09886fd 100644 --- a/src/main/java/com/firebolt/jdbc/util/CloseableUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/CloseableUtil.java @@ -1,14 +1,15 @@ package com.firebolt.jdbc.util; +import lombok.experimental.UtilityClass; + import java.io.Closeable; import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; -import lombok.CustomLog; -import lombok.experimental.UtilityClass; - -@CustomLog @UtilityClass public class CloseableUtil { + private static final Logger log = Logger.getLogger(CloseableUtil.class.getName()); /** * Closes the {@link Closeable} and log any potential {@link IOException} @@ -20,7 +21,7 @@ public void close(Closeable closeable) { try { closeable.close(); } catch (IOException e) { - log.error("An error happened while closing the closeable: {}", e.getMessage()); + log.log(Level.SEVERE, "An error happened while closing the closeable: {0}", e.getMessage()); } } } diff --git a/src/main/java/com/firebolt/jdbc/util/InputStreamUtil.java b/src/main/java/com/firebolt/jdbc/util/InputStreamUtil.java index 3c86b9e1..a1e66e03 100644 --- a/src/main/java/com/firebolt/jdbc/util/InputStreamUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/InputStreamUtil.java @@ -1,15 +1,19 @@ package com.firebolt.jdbc.util; -import lombok.CustomLog; import lombok.experimental.UtilityClass; import javax.annotation.Nullable; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; +import java.util.logging.Level; +import java.util.logging.Logger; @UtilityClass -@CustomLog public class InputStreamUtil { + private static final int K_BYTE = 1024; + private static final int BUFFER_SIZE = 8 * K_BYTE; + private static final Logger log = Logger.getLogger(InputStreamUtil.class.getName()); /** * Read all bytes from the input stream if the stream is not null @@ -22,9 +26,22 @@ public void readAllBytes(@Nullable InputStream is) { try { if (is.read() == -1) break; } catch (IOException e) { - log.warn("Could not read entire input stream for non query statement", e); + log.log(Level.WARNING, "Could not read entire input stream for non query statement", e); } } } } + + public String read(Reader initialReader, int limit) throws IOException { + char[] arr = new char[BUFFER_SIZE]; + StringBuilder buffer = new StringBuilder(); + int numCharsRead; + while ((numCharsRead = initialReader.read(arr, 0, arr.length)) != -1) { + buffer.append(arr, 0, numCharsRead); + if (buffer.length() >= limit) { + break; + } + } + return buffer.length() > limit ? buffer.substring(0, limit) : buffer.toString(); + } } diff --git a/src/main/java/com/firebolt/jdbc/util/LoggerUtil.java b/src/main/java/com/firebolt/jdbc/util/LoggerUtil.java index 3092885a..3aea5e03 100644 --- a/src/main/java/com/firebolt/jdbc/util/LoggerUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/LoggerUtil.java @@ -1,38 +1,39 @@ package com.firebolt.jdbc.util; -import java.io.*; +import com.firebolt.FireboltDriver; +import lombok.experimental.UtilityClass; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; -import com.firebolt.jdbc.log.FireboltLogger; -import com.firebolt.jdbc.log.JDKLogger; -import com.firebolt.jdbc.log.SLF4JLogger; - -import lombok.CustomLog; -import lombok.experimental.UtilityClass; - @UtilityClass -@CustomLog public class LoggerUtil { - private static Boolean slf4jAvailable; + private static final boolean SLF4J_AVAILABLE = isSlf4jJAvailable(); + private static final Logger root = initRootLogger(); + private static final Logger log = Logger.getLogger(LoggerUtil.class.getName()); - /** - * Provides a {@link FireboltLogger} based on whether SLF4J is available or not. - * - * @param name logger name - * @return a {@link FireboltLogger} - */ - public static FireboltLogger getLogger(String name) { - if (slf4jAvailable == null) { - slf4jAvailable = isSlf4jJAvailable(); + private Logger initRootLogger() { + Logger parent = Logger.getLogger(FireboltDriver.class.getPackageName()); + if (SLF4J_AVAILABLE) { + synchronized (LoggerUtil.class) { + parent.addHandler(new SLF4JBridgeHandler()); + parent.setLevel(Level.ALL); + } } + return parent; + } - if (slf4jAvailable) { - return new SLF4JLogger(name); - } else { - return new JDKLogger(name); - } + public static Logger getRootLogger() { + return root; } /** @@ -42,6 +43,7 @@ public static FireboltLogger getLogger(String name) { * @return a copy of the {@link InputStream} provided */ public InputStream logInputStream(InputStream is) { + @SuppressWarnings("SpellCheckingInspection") // BAOS means byte array output stream ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { byte[] buffer = new byte[1024]; @@ -58,7 +60,7 @@ public InputStream logInputStream(InputStream is) { log.info("======================================"); return new ByteArrayInputStream(baos.toByteArray()); } catch (Exception ex) { - log.warn("Could not log the stream", ex); + log.log(Level.WARNING, "Could not log the stream", ex); } return new ByteArrayInputStream(baos.toByteArray()); } diff --git a/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java b/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java index 7b460cfb..bf6a382d 100644 --- a/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/PropertyUtil.java @@ -1,22 +1,22 @@ package com.firebolt.jdbc.util; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; +import lombok.experimental.UtilityClass; + import java.sql.DriverPropertyInfo; -import java.util.ArrayList; +import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; import java.util.List; +import java.util.Map.Entry; import java.util.Optional; import java.util.Properties; import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; +import static com.firebolt.jdbc.connection.UrlUtil.extractProperties; +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.getNonDeprecatedProperties; -import com.firebolt.jdbc.connection.FireboltJdbcUrlUtil; -import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; - -import lombok.CustomLog; -import lombok.experimental.UtilityClass; - -@CustomLog @UtilityClass public class PropertyUtil { @@ -30,18 +30,7 @@ public class PropertyUtil { * @return an array containing the properties used by the driver */ public DriverPropertyInfo[] getPropertyInfo(String url, Properties properties) { - try { - Properties propertiesFromUrl = FireboltJdbcUrlUtil.extractProperties(url); - for (Object key : propertiesFromUrl.keySet()) { - properties.put(key, propertiesFromUrl.get(key.toString())); - } - } catch (Exception ex) { - log.error("Could not extract properties from url {}", url, ex); - } - - List result = new ArrayList<>( - mapProperties(FireboltSessionProperty.getNonDeprecatedProperties(), properties)); - return result.toArray(new DriverPropertyInfo[0]); + return mapProperties(getNonDeprecatedProperties(), mergeProperties(extractProperties(url), properties)).toArray(new DriverPropertyInfo[0]); } /** @@ -51,14 +40,14 @@ public DriverPropertyInfo[] getPropertyInfo(String url, Properties properties) { * @return true if the host property is localhost */ public boolean isLocalDb(FireboltProperties fireboltProperties) { - return StringUtils.equalsIgnoreCase(fireboltProperties.getHost(), LOCALHOST); + return LOCALHOST.equalsIgnoreCase(fireboltProperties.getHost()); } private List mapProperties(List fireboltSessionProperties, Properties properties) { return fireboltSessionProperties.stream().map(fireboltProperty -> { - DriverPropertyInfo driverPropertyInfo = new DriverPropertyInfo(fireboltProperty.getKey(), - getValueForFireboltSessionProperty(properties, fireboltProperty)); + Entry property = getValueForFireboltSessionProperty(properties, fireboltProperty); + DriverPropertyInfo driverPropertyInfo = new DriverPropertyInfo(property.getKey(), property.getValue()); driverPropertyInfo.required = false; driverPropertyInfo.description = fireboltProperty.getDescription(); driverPropertyInfo.choices = fireboltProperty.getPossibleValues(); @@ -66,11 +55,22 @@ private List mapProperties(List fir }).collect(Collectors.toList()); } - private String getValueForFireboltSessionProperty(Properties properties, - FireboltSessionProperty fireboltSessionProperty) { - Optional value = Optional.ofNullable(properties.getProperty(fireboltSessionProperty.getKey())); + private Entry getValueForFireboltSessionProperty(Properties properties, FireboltSessionProperty fireboltSessionProperty) { + String strDefaultValue = Optional.ofNullable(fireboltSessionProperty.getDefaultValue()).map(Object::toString).orElse(null); + return Stream.concat(Stream.of(fireboltSessionProperty.getKey()), Arrays.stream(fireboltSessionProperty.getAliases())) + .filter(key -> properties.getProperty(key) != null) + .map(key -> new SimpleEntry<>(key, properties.getProperty(key))) + .findFirst() + .orElseGet(() -> new SimpleEntry<>(fireboltSessionProperty.getKey(), strDefaultValue)); + } - return value.orElseGet(() -> Optional.ofNullable(fireboltSessionProperty.getDefaultValue()) - .map(Object::toString).orElse(null)); + public static Properties mergeProperties(Properties... properties) { + Properties mergedProperties = new Properties(); + for (Properties p : properties) { + if (p != null) { + mergedProperties.putAll(p); + } + } + return mergedProperties; } } diff --git a/src/main/java/com/firebolt/jdbc/util/StringUtil.java b/src/main/java/com/firebolt/jdbc/util/StringUtil.java new file mode 100644 index 00000000..542e8b6e --- /dev/null +++ b/src/main/java/com/firebolt/jdbc/util/StringUtil.java @@ -0,0 +1,53 @@ +package com.firebolt.jdbc.util; + + +import lombok.experimental.UtilityClass; + +import java.util.ArrayList; +import java.util.List; + +@UtilityClass +public class StringUtil { + private static final String[] EMPTY_STRING_ARRAY = {}; + private static final String[] SINGLE_STRING_ARRAY = { "" }; + + public static String strip(String value, char c) { + if (value == null || value.isEmpty()) { + return value; + } + int begin = value.charAt(0) == c ? 1 : 0; + int end = value.charAt(value.length() - 1) == c ? value.length() - 1 : value.length(); + return value.substring(begin, end); + } + + /** + * Based on {@code org.apache.commons.lang3.StringUtils#split(String, char)} with small modifications, simplification and optimization + * @param str – the nullable String to parse + * @param separatorChar – the character used as the delimiter, null splits on whitespace + * @return an array of parsed Strings, empty array if input is {@code null} and single element with empty string array if input is empty string + */ + public static String[] splitAll(String str, char separatorChar) { + if (str == null) { + return EMPTY_STRING_ARRAY; + } + final int len = str.length(); + if (len == 0) { + return SINGLE_STRING_ARRAY; + } + char[] chars = str.toCharArray(); + final List list = new ArrayList<>(); + int i = 0; + int start = 0; + while (i < len) { + if (chars[i] == separatorChar) { + list.add(new String(chars, start, i - start)); + start = ++i; + continue; + } + i++; + } + list.add(str.substring(start, i)); + return list.toArray(EMPTY_STRING_ARRAY); + } + +} diff --git a/src/main/java/com/firebolt/jdbc/util/VersionUtil.java b/src/main/java/com/firebolt/jdbc/util/VersionUtil.java index 069fb513..dbf9fc04 100644 --- a/src/main/java/com/firebolt/jdbc/util/VersionUtil.java +++ b/src/main/java/com/firebolt/jdbc/util/VersionUtil.java @@ -1,8 +1,8 @@ package com.firebolt.jdbc.util; -import lombok.CustomLog; import lombok.experimental.UtilityClass; +import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -11,11 +11,12 @@ import java.util.Properties; import java.util.jar.Attributes.Name; import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; @UtilityClass -@CustomLog public class VersionUtil { private static final Pattern VERSION_PATTERN = Pattern.compile("^\\s*(\\d+)\\.(\\d+).*"); @@ -24,42 +25,97 @@ public class VersionUtil { private static final String SPECIFICATION_VERSION = "Specification-Version"; private static final String FIREBOLT_IMPLEMENTATION_TITLE = "Firebolt JDBC driver"; // This value must be the same as one defined in build.gradle/jar/manifest/attributes - private static String driverVersion; - private static String specificationVersion; + private static String driverVersion = "3.0.4"; + private static String specificationVersion = "4.3"; + private static final Logger log = Logger.getLogger(VersionUtil.class.getName()); static { try { retrieveVersionInfo(); - log.info("Firebolt driver version used: {}", driverVersion); + log.log(Level.INFO, "Firebolt driver version used: {0}", driverVersion); } catch (IOException e) { - log.error("Could not get Project Version defined in the build.gradle file", e); + log.log(Level.SEVERE, "Could not get Project Version defined in the build.gradle file", e); } } + /** + * Does the best effort to retrieve the driver version. + *
    + *
  1. parse MANIFEST.MF loaded by the same class loader that was used for loading of this class.
  2. + *
  3. Iterate over all MANIFEST.MF found in the classpath, identify correct manifest by its title "Firebolt JDBC driver" and parse it
  4. + *
  5. get driver version only from version.properties if found
  6. + *
  7. get driver and specification version from gradle.properties
  8. + *
  9. use hard coded version values - the last fallback
  10. + *
+ * + * Why this mechanism is so complicated? Theoretically, in single class loader application, the first mechanism must work. + * However, current class can be loaded by other class loader, so MANIFEST.MF will not be available there. In this case + * we do yet another attempt: iterate over all available for context class loader of current thread manifest files. + * This still may fail. So, we do yet another attempt: try to locate version.properties generated by the build. + * We also have here a patch for unit tests that run against not packaged classes when manifest is not created yet. + * This patch is implemented by using gradle.properties - the source of the current version, file that is manually + * updated when we want to move to the next version. This file however does not exist in production environment. + * The last fallback is hard coded driver and specification version. The specification version is being changed very + * seldom, so the chance that it will be wrong is low. Driver version is increased from time to time, so in worse + * case, if the real version is not found and developer forgot to update hard coded driver version together with one + * defined in the MANIFEST.MF (and sourced from the version.properties) the driver will report wrong version. + * It is not good but anyway better than returning null that can cause failure in some systems. + * In our case we saw problem in Tableau that throws {@link NullPointerException} in this case. + * ({@see FIR-30343}) + * + * @throws IOException if something went really wrong + */ private static void retrieveVersionInfo() throws IOException { - for(Enumeration eurl = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF"); eurl.hasMoreElements();) { - URL url = eurl.nextElement(); - try (InputStream in = url.openStream()) { - Manifest manifest = new Manifest(in); - String implementationTitle = (String)manifest.getMainAttributes().get(new Name(IMPLEMENTATION_TITLE)); - if (FIREBOLT_IMPLEMENTATION_TITLE.equals(implementationTitle)) { - driverVersion = (String)manifest.getMainAttributes().get(new Name(IMPLEMENTATION_VERSION)); - specificationVersion = (String)manifest.getMainAttributes().get(new Name(SPECIFICATION_VERSION)); + if (retrieveVersionInfoFromManifest(VersionUtil.class.getResourceAsStream("/META-INF/MANIFEST.MF"))) { + return; + } + for(Enumeration eUrl = Thread.currentThread().getContextClassLoader().getResources("META-INF/MANIFEST.MF"); eUrl.hasMoreElements();) { + try (InputStream in = eUrl.nextElement().openStream()) { + if (retrieveVersionInfoFromManifest(in)) { return; } } } - try (InputStream in = new FileInputStream("gradle.properties")) { - Properties properties = new Properties(); - properties.load(in); - driverVersion = properties.getProperty("version"); - specificationVersion = properties.getProperty("jdbcVersion"); + if (retrieveVersionInfoFromProperties(VersionUtil.class.getResourceAsStream("/version.properties"))) { + return; + } + + File gradleProperties = new File("gradle.properties"); // for testing environment + if (gradleProperties.exists()) { + try (InputStream in = new FileInputStream(gradleProperties)) { + retrieveVersionInfoFromProperties(in); + } + } + } + + private static boolean retrieveVersionInfoFromManifest(InputStream in) throws IOException { + if (in == null) { + return false; + } + Manifest manifest = new Manifest(in); + String implementationTitle = (String)manifest.getMainAttributes().get(new Name(IMPLEMENTATION_TITLE)); + if (FIREBOLT_IMPLEMENTATION_TITLE.equals(implementationTitle)) { + driverVersion = (String)manifest.getMainAttributes().get(new Name(IMPLEMENTATION_VERSION)); + specificationVersion = (String)manifest.getMainAttributes().get(new Name(SPECIFICATION_VERSION)); + return true; + } + return false; + } + + private static boolean retrieveVersionInfoFromProperties(InputStream in) throws IOException { + if (in == null) { + return false; } + Properties properties = new Properties(); + properties.load(in); + driverVersion = properties.getProperty("version", driverVersion); + specificationVersion = properties.getProperty("jdbcVersion", specificationVersion); + return true; } /** * Returns the driver major version - * + * * @return the driver major version */ public int getMajorDriverVersion() { @@ -68,7 +124,7 @@ public int getMajorDriverVersion() { /** * Returns the driver minor version - * + * * @return the driver minor version */ public int getDriverMinorVersion() { @@ -77,7 +133,7 @@ public int getDriverMinorVersion() { /** * Extracts the major version from the version provided - * + * * @param version the version to extract the major version from * @return the major version */ @@ -91,7 +147,7 @@ public int extractMajorVersion(String version) { /** * Extracts the minor version from the version provided - * + * * @param version the version to extract the minor version from * @return the minor version */ @@ -105,7 +161,7 @@ public int extractMinorVersion(String version) { /** * Returns the driver version - * + * * @return the driver version */ public String getDriverVersion() { diff --git a/src/test/java/com/firebolt/FireboltDriverTest.java b/src/test/java/com/firebolt/FireboltDriverTest.java index b63594f1..7808418f 100644 --- a/src/test/java/com/firebolt/FireboltDriverTest.java +++ b/src/test/java/com/firebolt/FireboltDriverTest.java @@ -1,20 +1,23 @@ package com.firebolt; -import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.FireboltConnectionUserPassword; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockedConstruction; +import java.io.IOException; +import java.io.StringReader; +import java.sql.Connection; import java.sql.DriverPropertyInfo; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; +import java.util.logging.Logger; import java.util.stream.Collector; import java.util.stream.Collectors; @@ -22,7 +25,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mockConstruction; class FireboltDriverTest { @@ -32,11 +34,35 @@ void shouldNotReturnNewConnectionWhenUrlIsInvalid(String url) throws SQLExceptio assertNull(new FireboltDriver().connect(url, null)); } - @Test - void shouldReturnNewConnectionWhenUrlIsValid() throws SQLException { - try (MockedConstruction mocked = mockConstruction(FireboltConnection.class)) { + @ParameterizedTest + @CsvSource({ + "FireboltConnectionServiceSecret, jdbc:firebolt://api.dev.firebolt.io/db_name,", // old URL, null properties, i.e. no username - for "URL backwards compatibility" considered v2 + "FireboltConnectionServiceSecret, jdbc:firebolt://api.dev.firebolt.io/db_name,''", // same but empty properties + "FireboltConnectionServiceSecret,jdbc:firebolt:db_name,", // new URL format, null properties + "FireboltConnectionServiceSecret,jdbc:firebolt:db_name,''", // same but empty properties + "FireboltConnectionUserPassword, jdbc:firebolt://api.dev.firebolt.io/db_name,'user=sherlok@holmes.uk;password=watson'", // user is email - v1 + "FireboltConnectionServiceSecret, jdbc:firebolt://api.dev.firebolt.io/db_name,'user=not-email;password=any'", // user is not email - v2 + "FireboltConnectionServiceSecret, jdbc:firebolt://api.dev.firebolt.io/db_name,'client_id=not-email;client_secret=any'", // clientId and client_secret are defined - v2 + "FireboltConnectionUserPassword, jdbc:firebolt://api.dev.firebolt.io/db_name?user=sherlok@holmes.uk&password=watson,", // user is email as URL parameter - v1 // legit:ignore-secrets + "FireboltConnectionServiceSecret, jdbc:firebolt://api.dev.firebolt.io/db_name?client_id=not-email&client_secret=any,", // clientId and client_secret as URL parameters - v2 + "FireboltConnectionUserPassword, jdbc:firebolt://api.dev.firebolt.io/db_name?access_token=aaabbbccc,", // old URL, no credentials but with access token + "FireboltConnectionServiceSecret,jdbc:firebolt:db_name,", // new URL, no credentials but with access token + }) + void validateConnectionWhenUrlIsValid(String expectedConnectionTypeName, String jdbcUrl, String propsString) throws SQLException, IOException, ClassNotFoundException { + Properties properties = null; + if (propsString != null) { + properties = new Properties(); + properties.load(new StringReader(propsString)); + } + @SuppressWarnings("unchecked") + Class expectedConnectionType = (Class)Class.forName(FireboltConnectionUserPassword.class.getPackageName() + "." + expectedConnectionTypeName); + validateConnection(expectedConnectionType, jdbcUrl, properties); + } + + private void validateConnection(Class expectedConnectionType, String jdbcUrl, Properties properties) throws SQLException { + try (MockedConstruction mocked = mockConstruction(expectedConnectionType)) { FireboltDriver fireboltDriver = new FireboltDriver(); - assertNotNull(fireboltDriver.connect("jdbc:firebolt://api.dev.firebolt.io/db_name", new Properties())); + assertNotNull(fireboltDriver.connect(jdbcUrl, properties)); assertEquals(1, mocked.constructed().size()); } } @@ -54,7 +80,8 @@ void acceptsURL(String url, boolean expected) { @Test void getParentLogger() { - assertThrows(SQLFeatureNotSupportedException.class, () -> new FireboltDriver().getParentLogger()); + Logger logger = new FireboltDriver().getParentLogger(); + assertNotNull(logger); } @Test @@ -65,8 +92,8 @@ void jdbcCompliant() { @Test void version() { FireboltDriver fireboltDriver = new FireboltDriver(); - assertEquals(2, fireboltDriver.getMajorVersion()); - assertEquals(4, fireboltDriver.getMinorVersion()); + assertEquals(3, fireboltDriver.getMajorVersion()); + assertEquals(0, fireboltDriver.getMinorVersion()); } @ParameterizedTest @@ -76,10 +103,13 @@ void version() { "jdbc:firebolt://api.dev.firebolt.io/db_name,,host=api.dev.firebolt.io;path=/db_name", "jdbc:firebolt://api.dev.firebolt.io/db_name?account=test,,host=api.dev.firebolt.io;path=/db_name;account=test", "jdbc:firebolt://api.dev.firebolt.io/db_name?account=test,user=usr;password=pwd,host=api.dev.firebolt.io;path=/db_name;account=test;user=usr;password=pwd", // legit:ignore-secrets - "jdbc:firebolt://api.dev.firebolt.io/db_name,user=usr;password=pwd,host=api.dev.firebolt.io;path=/db_name;user=usr;password=pwd" // legit:ignore-secrets + "jdbc:firebolt://api.dev.firebolt.io/db_name,user=usr;password=pwd,host=api.dev.firebolt.io;path=/db_name;user=usr;password=pwd", // legit:ignore-secrets + // TODO: add more tests with "new" URL format +// "jdbc:firebolt:db_name,,host=api.dev.firebolt.io;database=db_name", + }, delimiter = ',') - void getPropertyInfo(String url, String propStr, String expectedInfoStr) throws SQLException { + void getPropertyInfo(String url, String propStr, String expectedInfoStr) { Properties expectedProps = toProperties(expectedInfoStr); assertEquals(expectedProps, toMap(new FireboltDriver().getPropertyInfo(url, toProperties(propStr))).entrySet().stream().filter(e -> expectedProps.containsKey(e.getKey())).collect(Collectors.toMap(Entry::getKey, Entry::getValue))); } diff --git a/src/test/java/com/firebolt/jdbc/RawStatementTest.java b/src/test/java/com/firebolt/jdbc/RawStatementTest.java index 20e03ade..197b23ac 100644 --- a/src/test/java/com/firebolt/jdbc/RawStatementTest.java +++ b/src/test/java/com/firebolt/jdbc/RawStatementTest.java @@ -6,6 +6,7 @@ import java.util.Arrays; import java.util.Collections; +import com.firebolt.jdbc.metadata.MetadataUtil.Query; import org.junit.jupiter.api.Test; class RawStatementTest { diff --git a/src/test/java/com/firebolt/jdbc/client/FireboltClientTest.java b/src/test/java/com/firebolt/jdbc/client/FireboltClientTest.java index 443e5134..fa021204 100644 --- a/src/test/java/com/firebolt/jdbc/client/FireboltClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/FireboltClientTest.java @@ -1,6 +1,7 @@ package com.firebolt.jdbc.client; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; @@ -10,11 +11,17 @@ import okhttp3.Response; import okhttp3.ResponseBody; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.sql.SQLException; +import java.util.stream.Stream; import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; @@ -98,8 +105,18 @@ void cannotExtractCompressedErrorMessage() throws IOException { } } - @Test - void canExtractErrorMessage() throws IOException { + // FIR-33934: This test does not validate the fields of ServerError except error message including Location because this information is not exposed to FireboltException + @ParameterizedTest + @CsvSource(value = { + "Error happened; Error happened", + "Error happened on server: Line 16, Column 64: Something bad happened; Something bad happened", + "{}; null", + "{\"errors:\": [null]}; null", + "{errors: [{\"name\": \"Something wrong happened\"}]}; Something wrong happened", + "{errors: [{\"description\": \"Error happened on server: Line 16, Column 64: Something bad happened\"}]}; Something bad happened", + "{errors: [{\"description\": \"Error happened on server: Line 16, Column 64: Something bad happened\", \"location\": {\"failingLine\": 20, \"startOffset\": 30, \"endOffset\": 40}}]}; Something bad happened" + }, delimiter = ';') + void canExtractErrorMessage(String rawMessage, String expectedMessage) throws IOException { try (Response response = mock(Response.class)) { when(response.code()).thenReturn(HTTP_NOT_FOUND); ResponseBody responseBody = mock(ResponseBody.class); @@ -107,7 +124,7 @@ void canExtractErrorMessage() throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); OutputStream compressedStream = new LZ4OutputStream(baos, 100); - compressedStream.write("Error happened".getBytes()); + compressedStream.write(rawMessage.getBytes()); compressedStream.flush(); compressedStream.close(); when(responseBody.bytes()).thenReturn(baos.toByteArray()); // compressed error message @@ -115,7 +132,7 @@ void canExtractErrorMessage() throws IOException { FireboltClient client = Mockito.mock(FireboltClient.class, Mockito.CALLS_REAL_METHODS); FireboltException e = assertThrows(FireboltException.class, () -> client.validateResponse("the_host", response, true)); assertEquals(ExceptionType.RESOURCE_NOT_FOUND, e.getType()); - assertTrue(e.getMessage().contains("Error happened")); // compressed error message is used as-is + assertTrue(e.getMessage().contains(expectedMessage)); // compressed error message is used as-is } } @@ -128,11 +145,57 @@ void emptyResponseFromServer() throws IOException { Call call = mock(); when(call.execute()).thenReturn(response); when(okHttpClient.newCall(any())).thenReturn(call); - FireboltClient client = new FireboltClient(okHttpClient, mock(), null, null, new ObjectMapper()) {}; + FireboltClient client = new FireboltClient(okHttpClient, mock(), null, null) {}; assertEquals("Cannot get resource: the response from the server is empty", assertThrows(FireboltException.class, () -> client.getResource("http://foo", "foo", "token", String.class)).getMessage()); } } + private static Stream goodJson() { + return Stream.of( + Arguments.of(GatewayUrlResponse.class, "{\"engineUrl\": \"my.engine\"}", new GatewayUrlResponse("my.engine")), + Arguments.of(FireboltAccount.class, "{\"id\": \"123\", \"region\": \"earth\"}", new FireboltAccount("123", "earth", 1)), + Arguments.of(GatewayUrlResponse.class, null, null), + Arguments.of(FireboltAccount.class, null, null) + ); + } + + @ParameterizedTest(name = "{0}:{1}") + @MethodSource("goodJson") + void goodJsonResponse(Class clazz, String json, T expected) throws SQLException, IOException { + assertEquals(expected, mockClient(json).getResource("http://foo", "foo", "token", clazz)); + } + + private static Stream badJson() { + return Stream.of( + Arguments.of(GatewayUrlResponse.class, "", "A JSONObject text must begin with '{' at 0 [character 1 line 1]"), + Arguments.of(FireboltAccount.class, "", "A JSONObject text must begin with '{' at 0 [character 1 line 1]"), + Arguments.of(GatewayUrlResponse.class, "{}", "JSONObject[\"engineUrl\"] not found."), + Arguments.of(FireboltAccount.class, "{}", "JSONObject[\"id\"] not found.") + ); + } + + @ParameterizedTest(name = "{0}:{1}") + @MethodSource("badJson") + void wrongJsonResponse(Class clazz, String json, String expectedErrorMessage) throws IOException { + FireboltClient client = mockClient(json); + IOException e = assertThrows(IOException.class,() -> client.getResource("http://foo", "foo", "token", clazz)); + assertEquals(expectedErrorMessage, e.getMessage()); + } + + private FireboltClient mockClient(String json) throws IOException { + try (Response response = mock(Response.class)) { + when(response.code()).thenReturn(200); + ResponseBody responseBody = mock(ResponseBody.class); + when(responseBody.string()).thenReturn(json); + when(response.body()).thenReturn(responseBody); + OkHttpClient okHttpClient = mock(OkHttpClient.class); + Call call = mock(); + when(call.execute()).thenReturn(response); + when(okHttpClient.newCall(any())).thenReturn(call); + return new FireboltClient(okHttpClient, mock(), null, null) {}; + } + } + private Response mockResponse(int code) { Response response = mock(Response.class); ResponseBody responseBody = mock(ResponseBody.class); diff --git a/src/test/java/com/firebolt/jdbc/client/FireboltObjectMapperTest.java b/src/test/java/com/firebolt/jdbc/client/FireboltObjectMapperTest.java deleted file mode 100644 index 653fff24..00000000 --- a/src/test/java/com/firebolt/jdbc/client/FireboltObjectMapperTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.firebolt.jdbc.client; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.ObjectMapper; - -class FireboltObjectMapperTest { - - @Test - void shouldGetInstance() { - assertInstanceOf(ObjectMapper.class, FireboltObjectMapper.getInstance()); - } -} diff --git a/src/test/java/com/firebolt/jdbc/client/HttpClientConfigTest.java b/src/test/java/com/firebolt/jdbc/client/HttpClientConfigTest.java index 500dfbc2..90cfeec8 100644 --- a/src/test/java/com/firebolt/jdbc/client/HttpClientConfigTest.java +++ b/src/test/java/com/firebolt/jdbc/client/HttpClientConfigTest.java @@ -2,8 +2,10 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import java.lang.reflect.Field; +import java.util.Properties; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,8 +17,7 @@ class HttpClientConfigTest { @BeforeEach - public void resetSingleton() - throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + public void resetSingleton() throws ReflectiveOperationException { Field instance = HttpClientConfig.class.getDeclaredField("instance"); instance.setAccessible(true); instance.set(null, null); @@ -24,9 +25,10 @@ public void resetSingleton() @Test void shouldInitHttpClient() throws Exception { - OkHttpClient client = HttpClientConfig - .init(FireboltProperties.builder().maxConnectionsTotal(1).keepAliveTimeoutMillis(1).build()); + assertNull(HttpClientConfig.getInstance()); + OkHttpClient client = HttpClientConfig.init(new FireboltProperties(new Properties())); assertNotNull(client); + assertSame(client, HttpClientConfig.getInstance()); } @Test diff --git a/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java b/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java index 2ba83746..6ceb7483 100644 --- a/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/account/FireboltAccountClientTest.java @@ -1,213 +1,121 @@ package com.firebolt.jdbc.client.account; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; -import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import okhttp3.Call; import okhttp3.OkHttpClient; -import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.junit.function.ThrowingRunnable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; -import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import java.util.HashMap; -import java.util.Map; +import java.io.IOException; +import java.sql.SQLException; -import static com.firebolt.jdbc.client.UserAgentFormatter.userAgent; +import static com.firebolt.jdbc.exception.ExceptionType.RESOURCE_NOT_FOUND; import static java.lang.String.format; -import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; import static java.net.HttpURLConnection.HTTP_NOT_FOUND; import static java.net.HttpURLConnection.HTTP_OK; -import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FireboltAccountClientTest { - private static final String ACCESS_TOKEN = "token"; - private static final String HOST = "https://host"; - private static final String ACCOUNT = "account"; - private static final String ACCOUNT_ID = "account_id"; - private static final String DB_NAME = "dbName"; - private static final String ENGINE_NAME = "engineName"; - - @Spy - private final ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - - private final ObjectMapper mapper = new ObjectMapper(); - @Captor - private ArgumentCaptor requestArgumentCaptor; - @Mock - private OkHttpClient httpClient; - @Mock - private Call call; - private FireboltAccountClient fireboltAccountClient; - - @Mock - private FireboltConnection fireboltConnection; - - @BeforeEach - void setUp() { - fireboltAccountClient = new FireboltAccountClient(httpClient, objectMapper, fireboltConnection, "ConnA:1.0.9", - "ConnB:2.0.9"); - when(httpClient.newCall(any())).thenReturn(call); - } - - @Test - void shouldGetAccountId() throws Exception { - Response response = mock(Response.class); - when(response.code()).thenReturn(HTTP_OK); - ResponseBody body = mock(ResponseBody.class); - when(body.string()).thenReturn("{\"account_id\":\"12345\"}"); - when(response.body()).thenReturn(body); - when(call.execute()).thenReturn(response); - - FireboltAccountResponse account = fireboltAccountClient.getAccount(HOST, ACCOUNT, ACCESS_TOKEN); - - Map expectedHeader = Map.of("User-Agent", - userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9"), "Authorization", - "Bearer " + ACCESS_TOKEN); - - verify(httpClient).newCall(requestArgumentCaptor.capture()); - verify(objectMapper).readValue("{\"account_id\":\"12345\"}", FireboltAccountResponse.class); - assertEquals("https://host/iam/v2/accounts:getIdByName?accountName=" + ACCOUNT, - requestArgumentCaptor.getValue().url().toString()); - assertEquals(expectedHeader, extractHeadersMap(requestArgumentCaptor.getValue())); - assertEquals("12345", account.getAccountId()); - } - - @Test - void shouldGetEngineEndpoint() throws Exception { - Response response = mock(Response.class); - when(response.code()).thenReturn(HTTP_OK); - ResponseBody body = mock(ResponseBody.class); - when(response.body()).thenReturn(body); - when(call.execute()).thenReturn(response); - when(response.body().string()).thenReturn(mapper.writeValueAsString(FireboltEngineResponse.builder() - .engine(FireboltEngineResponse.Engine.builder().endpoint("http://engineEndpoint").build()).build())); - when(httpClient.newCall(any())).thenReturn(call); - - FireboltEngineResponse engine = fireboltAccountClient.getEngine(HOST, ENGINE_NAME, DB_NAME, ACCOUNT_ID, - ACCESS_TOKEN); - Map expectedHeader = Map.of("User-Agent", - userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9"), "Authorization", - "Bearer " + ACCESS_TOKEN); - - verify(httpClient).newCall(requestArgumentCaptor.capture()); - verify(objectMapper).readValue("{\"engine\":{\"endpoint\":\"http://engineEndpoint\",\"current_status\":null}}", - FireboltEngineResponse.class); - verify(httpClient).newCall(requestArgumentCaptor.capture()); - assertEquals("https://host/core/v1/accounts/engineName/engines/" + ACCOUNT_ID, - requestArgumentCaptor.getValue().url().toString()); - assertEquals(expectedHeader, extractHeadersMap(requestArgumentCaptor.getValue())); - assertEquals("http://engineEndpoint", engine.getEngine().getEndpoint()); - } - - @Test - void shouldGetDbAddress() throws Exception { - Response response = mock(Response.class); - when(response.code()).thenReturn(HTTP_OK); - ResponseBody body = mock(ResponseBody.class); - when(response.body()).thenReturn(body); - when(call.execute()).thenReturn(response); - when(response.body().string()).thenReturn(mapper - .writeValueAsString(FireboltDefaultDatabaseEngineResponse.builder().engineUrl("http://dbAddress").build())); - - FireboltDefaultDatabaseEngineResponse fireboltDefaultDatabaseEngineResponse = fireboltAccountClient - .getDefaultEngineByDatabaseName(HOST, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); - Map expectedHeader = Map.of("User-Agent", - userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9"), "Authorization", - "Bearer " + ACCESS_TOKEN); - - verify(httpClient).newCall(requestArgumentCaptor.capture()); - verify(objectMapper).readValue("{\"engine_url\":\"http://dbAddress\"}", FireboltDefaultDatabaseEngineResponse.class); - assertEquals(format("https://host/core/v1/accounts/%s/engines:getURLByDatabaseName?databaseName=%s", - ACCOUNT_ID, DB_NAME), requestArgumentCaptor.getValue().url().toString()); - assertEquals(expectedHeader, extractHeadersMap(requestArgumentCaptor.getValue())); - assertEquals("http://dbAddress", fireboltDefaultDatabaseEngineResponse.getEngineUrl()); - } - - @Test - void shouldGetEngineId() throws Exception { - Response response = mock(Response.class); - when(response.code()).thenReturn(HTTP_OK); - ResponseBody body = mock(ResponseBody.class); - when(response.body()).thenReturn(body); - when(call.execute()).thenReturn(response); - when(response.body().string()).thenReturn(mapper.writeValueAsString(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId("13").build()).build())); - - FireboltEngineIdResponse fireboltEngineIdResponse = fireboltAccountClient.getEngineId(HOST, ACCOUNT_ID, - ENGINE_NAME, ACCESS_TOKEN); - Map expectedHeader = Map.of("User-Agent", - userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9"), "Authorization", - "Bearer " + ACCESS_TOKEN); - - verify(httpClient).newCall(requestArgumentCaptor.capture()); - verify(objectMapper).readValue("{\"engine_id\":{\"engine_id\":\"13\"}}", FireboltEngineIdResponse.class); - assertEquals(format("https://host/core/v1/accounts/%s/engines:getIdByName?engine_name=%s", ACCOUNT_ID, - ENGINE_NAME), requestArgumentCaptor.getValue().url().toString()); - assertEquals(expectedHeader, extractHeadersMap(requestArgumentCaptor.getValue())); - assertEquals("13", fireboltEngineIdResponse.getEngine().getEngineId()); - } - - @Test - void shouldThrowExceptionWhenStatusCodeIsNotFound() throws Exception { - shouldThrowException(HTTP_NOT_FOUND, () -> fireboltAccountClient.getAccount(HOST, ACCOUNT, ACCESS_TOKEN), null); - } - - @Test - void shouldThrowExceptionWhenStatusCodeIsNotOk() throws Exception { - shouldThrowException(HTTP_BAD_GATEWAY, () -> fireboltAccountClient.getAccount(HOST, ACCOUNT, ACCESS_TOKEN), null); - } - - @Test - void shouldThrowExceptionWithDBNotFoundErrorMessageWhenDBIsNotFound() throws Exception { - shouldThrowException(HTTP_NOT_FOUND, () -> fireboltAccountClient.getDefaultEngineByDatabaseName(HOST, ACCOUNT, DB_NAME, ACCESS_TOKEN), "The database with the name dbName could not be found"); - } - - @Test - void shouldThrowExceptionWithEngineNotFoundErrorMessageWhenEngineAddressIsNotFound() throws Exception { - shouldThrowException(HTTP_NOT_FOUND, () -> fireboltAccountClient.getEngine(HOST, ACCOUNT, ENGINE_NAME, "123", ACCESS_TOKEN), "The address of the engine with name engineName and id 123 could not be found"); - } - - @Test - void shouldThrowExceptionWithEngineNotFoundErrorMessageWhenEngineIdIsNotFound() throws Exception { - shouldThrowException(HTTP_NOT_FOUND, () -> fireboltAccountClient.getEngineId(HOST, ACCOUNT, ENGINE_NAME, ACCESS_TOKEN), "The engine engineName could not be found"); - } - - private void shouldThrowException(int httpStatus, ThrowingRunnable runnable, String expectedMessage) throws Exception { - Response response = mock(Response.class); - when(response.code()).thenReturn(httpStatus); - ResponseBody body = mock(ResponseBody.class); - when(response.body()).thenReturn(body); - when(call.execute()).thenReturn(response); - FireboltException fireboltException = assertThrows(FireboltException.class, runnable); - if (expectedMessage != null) { - assertEquals(expectedMessage, fireboltException.getMessage()); - } - } - - private Map extractHeadersMap(Request request) { - Map headers = new HashMap<>(); - request.headers().forEach(header -> headers.put(header.getFirst(), header.getSecond())); - return headers; - } -} + @Mock + private OkHttpClient httpClient; + @Mock + private FireboltConnection fireboltConnection; + private FireboltAccountClient client; + + @BeforeEach + void setUp() { + client = new FireboltAccountClient(httpClient, fireboltConnection, null, null); + } + + @Test + void getAccount() throws SQLException, IOException { + client.cleanup(); + String accountId = "123"; + injectMockedResponse(httpClient, HTTP_OK, format("{\"account_id\":\"%s\"}", accountId)); // FireboltAccountResponse + assertEquals(accountId, client.getAccount("http://host", "account", "token").getAccountId()); + verify(httpClient, times(1)).newCall(any()); + assertEquals(accountId, client.getAccount("http://host", "account", "token").getAccountId()); + verify(httpClient, times(1)).newCall(any()); + client.cleanup(); + assertEquals(accountId, client.getAccount("http://host", "account", "token").getAccountId()); + verify(httpClient, times(2)).newCall(any()); + } + + @Test + void getEngine() throws SQLException, IOException { + String endpoint = "http://engine/12345"; + injectMockedResponse(httpClient, HTTP_OK, format("{\"engine\": {\"endpoint\": \"%s\", \"current_status\": \"%s\"}}", endpoint, "running")); // FireboltEngineResponse + assertEquals("http://engine/12345", client.getEngine("http://host", "account-id", "engine", "engine-id", "token").getEngine().getEndpoint()); + } + + @Test + void getDefaultEngineByDatabaseName() throws SQLException, IOException { + String endpoint = "http://engine/12345"; + injectMockedResponse(httpClient, HTTP_OK, format("{\"engine_url\": \"%s\"}", endpoint)); // FireboltDefaultDatabaseEngineResponse + assertEquals(endpoint, client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token").getEngineUrl()); + } + + @Test + void getEngineId() throws SQLException, IOException { + String engineId = "456"; + injectMockedResponse(httpClient, HTTP_OK, format("{\"engine_id\": {\"engine_id\":\"%s\"}}", engineId)); // FireboltEngineIdResponse + assertEquals(engineId, client.getEngineId("http://host", "account-id", "db", "token").getEngine().getEngineId()); + } + + @Test + void notFoundError() throws IOException { + injectMockedResponse(httpClient, HTTP_NOT_FOUND, null); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getEngine("http://host", "account-id", "engine", "engine-id", "token"))); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token"))); + assertNotFoundException(assertThrows(FireboltException.class, () -> client.getEngineId("http://host", "account-id", "db", "token"))); + } + + @ParameterizedTest + @CsvSource(value = { + "404, RESOURCE_NOT_FOUND", + "400, INVALID_REQUEST", + "401, UNAUTHORIZED", + "429, TOO_MANY_REQUESTS", + "406, ERROR" + }) + void httpFailures(int httpStatus, ExceptionType type) throws IOException { + injectMockedResponse(httpClient, httpStatus, null); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getEngine("http://host", "account-id", "engine", "engine-id", "token")).getType()); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getDefaultEngineByDatabaseName("http://host", "account-id", "db", "token")).getType()); + assertEquals(type, assertThrows(FireboltException.class, () -> client.getEngineId("http://host", "account-id", "db", "token")).getType()); + } + + private void assertNotFoundException(FireboltException e) { + assertEquals(RESOURCE_NOT_FOUND, e.getType()); + assertTrue(e.getMessage().contains("could not be found")); + } + + private void injectMockedResponse(OkHttpClient httpClient, int code, String json) throws IOException { + Response response = mock(Response.class); + Call call = mock(Call.class); + when(httpClient.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(response); + ResponseBody body = mock(ResponseBody.class); + when(response.body()).thenReturn(body); + when(response.code()).thenReturn(code); + lenient().when(body.string()).thenReturn(json); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactoryTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactoryTest.java deleted file mode 100644 index 4a0aaebe..00000000 --- a/src/test/java/com/firebolt/jdbc/client/authentication/AuthenticationRequestFactoryTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.firebolt.jdbc.client.authentication; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.junit.jupiter.api.Test; - -class AuthenticationRequestFactoryTest { - - @Test - void shouldGetServiceAccountRequestWhenUsernameDoesNotContainSpecialCharacter() { - String name = "265576ea-2478-4209-860c-f75f55e7c1f7"; - String password = "hello"; - AuthenticationRequest rq = AuthenticationRequestFactory.getAuthenticationRequest(name, password, "localhost"); - assertTrue(rq instanceof ServiceAccountAuthenticationRequest); - } - - @Test - void shouldGetUsernamePasswordRqWhenUsernameIsAnEmailAddress() { - String name = "tester@firebolt.io"; - String password = "hello"; - AuthenticationRequest rq = AuthenticationRequestFactory.getAuthenticationRequest(name, password, "localhost"); - assertTrue(rq instanceof UsernamePasswordAuthenticationRequest); - } - - @Test - void shouldGetUsernamePasswordRqWhenUsernameIsNullOrEmpty() { - assertTrue(AuthenticationRequestFactory.getAuthenticationRequest(null, null, - null) instanceof UsernamePasswordAuthenticationRequest); - assertTrue(AuthenticationRequestFactory.getAuthenticationRequest("", null, - null) instanceof UsernamePasswordAuthenticationRequest); - } -} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java index 3e6c3e5a..a65a1ae2 100644 --- a/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java +++ b/src/test/java/com/firebolt/jdbc/client/authentication/FireboltAuthenticationClientTest.java @@ -1,28 +1,39 @@ package com.firebolt.jdbc.client.authentication; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.firebolt.jdbc.client.authentication.response.FireboltAuthenticationResponse; import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.exception.FireboltException; -import okhttp3.*; +import com.firebolt.jdbc.exception.SQLState; + +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Spy; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.io.IOException; +import java.sql.SQLException; import static com.firebolt.jdbc.client.UserAgentFormatter.userAgent; -import static java.net.HttpURLConnection.*; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FireboltAuthenticationClientTest { @@ -30,9 +41,7 @@ class FireboltAuthenticationClientTest { private static final String USER = "usr"; private static final String PASSWORD = "PA§§WORD"; - @Spy - private final ObjectMapper objectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + private static final String ENV = "ENV"; @Captor private ArgumentCaptor requestArgumentCaptor; @@ -45,12 +54,18 @@ class FireboltAuthenticationClientTest { @BeforeEach void setUp() { - fireboltAuthenticationClient = new FireboltAuthenticationClient(httpClient, objectMapper, connection, - "ConnA:1.0.9", "ConnB:2.0.9"); + fireboltAuthenticationClient = new FireboltAuthenticationClient(httpClient, connection, "ConnA:1.0.9", "ConnB:2.0.9") { + @Override + protected AuthenticationRequest getAuthenticationRequest(String username, String password, String host, String environment) { + AuthenticationRequest request = Mockito.mock(AuthenticationRequest.class); + when(request.getUri()).thenReturn("http://host/auth"); + return request; + } + }; } @Test - void shouldPostConnectionTokens() throws IOException, FireboltException { + void shouldPostConnectionTokens() throws SQLException, IOException { Response response = mock(Response.class); Call call = mock(Call.class); ResponseBody body = mock(ResponseBody.class); @@ -58,17 +73,17 @@ void shouldPostConnectionTokens() throws IOException, FireboltException { when(response.code()).thenReturn(HTTP_OK); when(httpClient.newCall(any())).thenReturn(call); when(call.execute()).thenReturn(response); - String tokensResponse = new ObjectMapper().writeValueAsString( - FireboltAuthenticationResponse.builder().accessToken("a").refreshToken("r").expiresIn(1).build()); + String tokensResponse = "{\"access_token\":\"a\", \"refresh_token\":\"r\", \"expires_in\":1}"; when(body.string()).thenReturn(tokensResponse); - fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD); + FireboltConnectionTokens tokens = fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV); + assertEquals("a", tokens.getAccessToken()); + assertEquals(1, tokens.getExpiresInSeconds()); verify(httpClient).newCall(requestArgumentCaptor.capture()); Request actualPost = requestArgumentCaptor.getValue(); assertEquals("User-Agent", actualPost.headers().iterator().next().getFirst()); assertEquals(userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9"), actualPost.headers().iterator().next().getSecond()); - verify(objectMapper).readValue(tokensResponse, FireboltAuthenticationResponse.class); } @Test @@ -82,7 +97,7 @@ void shouldThrowExceptionWhenStatusCodeIsNotFound() throws Exception { when(httpClient.newCall(any())).thenReturn(call); assertThrows(FireboltException.class, - () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD)); + () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); } @Test @@ -91,7 +106,7 @@ void shouldNotRetryWhenFacingANonRetryableException() throws Exception { when(call.execute()).thenThrow(IOException.class); when(httpClient.newCall(any())).thenReturn(call); - assertThrows(IOException.class, () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD)); + assertThrows(IOException.class, () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); verify(call).execute(); verify(call, times(0)).clone(); } @@ -106,7 +121,21 @@ void shouldThrowExceptionWhenStatusCodeIsForbidden() throws Exception { when(response.code()).thenReturn(HTTP_FORBIDDEN); when(httpClient.newCall(any())).thenReturn(call); - assertThrows(FireboltException.class, - () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD)); + FireboltException ex = assertThrows(FireboltException.class, + () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); + assertEquals(SQLState.INVALID_AUTHORIZATION_SPECIFICATION.getCode(), ex.getSQLState()); + } + + @Test + void shouldThrowExceptionWhenStatusCodeIsUnavailable() throws Exception { + Response response = mock(Response.class); + Call call = mock(Call.class); + when(call.execute()).thenReturn(response); + when(response.code()).thenReturn(HTTP_UNAVAILABLE); + when(httpClient.newCall(any())).thenReturn(call); + + FireboltException ex = assertThrows(FireboltException.class, + () -> fireboltAuthenticationClient.postConnectionTokens(HOST, USER, PASSWORD, ENV)); + assertEquals(SQLState.CONNECTION_FAILURE.getCode(), ex.getSQLState()); } } diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequestTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequestTest.java index 0df3cf7e..5d705858 100644 --- a/src/test/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequestTest.java +++ b/src/test/java/com/firebolt/jdbc/client/authentication/ServiceAccountAuthenticationRequestTest.java @@ -1,8 +1,11 @@ package com.firebolt.jdbc.client.authentication; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; @@ -14,18 +17,12 @@ class ServiceAccountAuthenticationRequestTest { @Test void shouldCreateHttpEntityWithTheProvidedCredentials() throws IOException { ServiceAccountAuthenticationRequest serviceAccountAuthenticationHttpRequest = new ServiceAccountAuthenticationRequest( - "he-ll-o", "secret", "https://api.dev.firebolt.io:443"); + "he-ll-o", "secret", "dev"); RequestBody requestBody = serviceAccountAuthenticationHttpRequest.getRequestBody(); Buffer buffer = new Buffer(); requestBody.writeTo(buffer); - assertEquals("client_id=he-ll-o&client_secret=secret&grant_type=client_credentials", buffer.readUtf8()); + assertEquals(format("audience=%s&grant_type=client_credentials&client_id=he-ll-o&client_secret=secret", URLEncoder.encode("https://api.firebolt.io", StandardCharsets.UTF_8)), buffer.readUtf8()); } - @Test - void shouldGetUri() { - ServiceAccountAuthenticationRequest serviceAccountAuthenticationHttpRequest = new ServiceAccountAuthenticationRequest( - "he-ll-o", "secret", "https://api.dev.firebolt.io:443"); - assertEquals("https://api.dev.firebolt.io:443/auth/v1/token", serviceAccountAuthenticationHttpRequest.getUri()); - } } \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequestTest.java b/src/test/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequestTest.java deleted file mode 100644 index f16b0bb5..00000000 --- a/src/test/java/com/firebolt/jdbc/client/authentication/UsernamePasswordAuthenticationRequestTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.firebolt.jdbc.client.authentication; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import org.junit.jupiter.api.Test; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import okhttp3.RequestBody; -import okio.Buffer; - -class UsernamePasswordAuthenticationRequestTest { - - @Test - void shouldCreateHttpEntityWithTheProvidedCredentials() throws IOException { - UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationHttpRequest = new UsernamePasswordAuthenticationRequest( - "hello", "pa$$word", "https://api.dev.firebolt.io:443"); - RequestBody requestBody = usernamePasswordAuthenticationHttpRequest.getRequestBody(); - Buffer buffer = new Buffer(); - requestBody.writeTo(buffer); - // We transform the requests to map because the order of the fields is not - // guaranteed - Map expectedRequest = new ObjectMapper() - .readValue("{\"username\":\"hello\",\"password\":\"pa$$word\"}", HashMap.class); - Map actualRequest = new ObjectMapper().readValue(buffer.readUtf8(), HashMap.class); - assertEquals(expectedRequest, actualRequest); - } - - @Test - void getUri() { - UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationHttpRequest = new UsernamePasswordAuthenticationRequest( - "hello", "pa$$word", "https://api.dev.firebolt.io:443"); - assertEquals("https://api.dev.firebolt.io:443/auth/v1/login", - usernamePasswordAuthenticationHttpRequest.getUri()); - } -} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java b/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java new file mode 100644 index 00000000..360eac32 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/client/gateway/FireboltGatewayUrlClientTest.java @@ -0,0 +1,188 @@ +package com.firebolt.jdbc.client.gateway; + +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.exception.FireboltException; +import okhttp3.Call; +import okhttp3.OkHttpClient; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.sql.SQLException; + +import static java.lang.String.format; +import static java.net.HttpURLConnection.HTTP_BAD_GATEWAY; +import static java.net.HttpURLConnection.HTTP_BAD_METHOD; +import static java.net.HttpURLConnection.HTTP_BAD_REQUEST; +import static java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_CONFLICT; +import static java.net.HttpURLConnection.HTTP_ENTITY_TOO_LARGE; +import static java.net.HttpURLConnection.HTTP_FORBIDDEN; +import static java.net.HttpURLConnection.HTTP_GATEWAY_TIMEOUT; +import static java.net.HttpURLConnection.HTTP_GONE; +import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR; +import static java.net.HttpURLConnection.HTTP_LENGTH_REQUIRED; +import static java.net.HttpURLConnection.HTTP_NOT_ACCEPTABLE; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_PAYMENT_REQUIRED; +import static java.net.HttpURLConnection.HTTP_PRECON_FAILED; +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; +import static java.net.HttpURLConnection.HTTP_REQ_TOO_LONG; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; +import static java.net.HttpURLConnection.HTTP_UNSUPPORTED_TYPE; +import static java.net.HttpURLConnection.HTTP_VERSION; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FireboltAccountRetrieverTest { + private static final String GENERIC_ERROR_MESSAGE = "Server failed to execute query with the following error:"; + + @Mock + private OkHttpClient httpClient; + + @Mock + private FireboltConnection fireboltConnection; + + private FireboltAccountRetriever fireboltGatewayUrlClient; + private FireboltAccountRetriever fireboltAccountIdResolver; + + + @BeforeEach + void setUp() { + fireboltGatewayUrlClient = new FireboltAccountRetriever<>(httpClient, fireboltConnection, null, null, "test-firebolt.io", "engineUrl", GatewayUrlResponse.class); + fireboltAccountIdResolver = new FireboltAccountRetriever<>(httpClient, fireboltConnection, null, null, "test-firebolt.io", "resolve", FireboltAccount.class); + } + + @Test + void shouldGetGatewayUrlWhenResponseIsOk() throws SQLException, IOException { + String engineUrl = "http://engine"; + injectMockedResponse(httpClient, HTTP_OK, format("{\"engineUrl\": \"%s\"}", engineUrl)); + assertEquals(engineUrl, fireboltGatewayUrlClient.retrieve("access_token", "account").getEngineUrl()); + } + + @Test + void shouldGetAccountId() throws SQLException, IOException { + fireboltAccountIdResolver.cleanup(); + FireboltAccount account = new FireboltAccount("12345", "central", 2); + injectMockedResponse(httpClient, HTTP_OK, "{\"id\": \"12345\", \"region\":\"central\", \"infraVersion\":2}"); + assertEquals(account, fireboltAccountIdResolver.retrieve("access_token", "account")); + Mockito.verify(httpClient, times(1)).newCall(any()); + // Do this again. The response is cached, so the invocation count will remain 1 + assertEquals(account, fireboltAccountIdResolver.retrieve("access_token", "account")); + // now clean the cache and call resolve() again. The invocation counter will be incremented. + fireboltAccountIdResolver.cleanup(); + assertEquals(account, fireboltAccountIdResolver.retrieve("access_token", "account")); + Mockito.verify(httpClient, times(2)).newCall(any()); + } + + @Test + void shouldRuntimeExceptionUponRuntimeException() { + when(httpClient.newCall(any())).thenThrow(new IllegalArgumentException("ex")); + assertEquals("ex", assertThrows(IllegalArgumentException.class, () -> fireboltGatewayUrlClient.retrieve("token", "acc")).getMessage()); + } + + @Test + void shouldThrowFireboltExceptionUponIOException() throws IOException { + Call call = mock(Call.class); + when(httpClient.newCall(any())).thenReturn(call); + when(call.execute()).thenThrow(new IOException("io error")); + assertEquals("Failed to get engineUrl url for account acc: io error", assertThrows(FireboltException.class, () -> fireboltGatewayUrlClient.retrieve("token", "acc")).getMessage()); + } + + @ParameterizedTest(name = "{0}:{1}") + @CsvSource({ + "resolve, com.firebolt.jdbc.client.account.FireboltAccount, {}, JSONObject[\"id\"] not found.", + "engineUrl, com.firebolt.jdbc.client.gateway.GatewayUrlResponse, {}, JSONObject[\"engineUrl\"] not found." + }) + void shouldThrowFireboltExceptionUponWrongJsonFormat(String path, Class clazz, String json, String expectedErrorMessage) throws IOException { + FireboltAccountRetriever fireboltAccountIdResolver = mockAccountRetriever(path, clazz, json); + assertEquals(format("Failed to get %s url for account acc: %s", path, expectedErrorMessage), assertThrows(FireboltException.class, () -> fireboltAccountIdResolver.retrieve("token", "acc")).getMessage()); + } + + private FireboltAccountRetriever mockAccountRetriever(String path, Class clazz, String json) throws IOException { + try (Response response = mock(Response.class)) { + when(response.code()).thenReturn(200); + ResponseBody responseBody = mock(ResponseBody.class); + when(responseBody.string()).thenReturn(json); + when(response.body()).thenReturn(responseBody); + OkHttpClient okHttpClient = mock(OkHttpClient.class); + Call call = mock(); + when(call.execute()).thenReturn(response); + when(okHttpClient.newCall(any())).thenReturn(call); + return new FireboltAccountRetriever<>(okHttpClient, mock(), null, null, "test-firebolt.io", path, clazz); + } + } + + @ParameterizedTest + @CsvSource({ + HTTP_BAD_REQUEST + "," + GENERIC_ERROR_MESSAGE, + HTTP_PAYMENT_REQUIRED + "," + GENERIC_ERROR_MESSAGE, + HTTP_BAD_METHOD + "," + GENERIC_ERROR_MESSAGE, + HTTP_NOT_ACCEPTABLE + "," + GENERIC_ERROR_MESSAGE, + HTTP_PROXY_AUTH + "," + GENERIC_ERROR_MESSAGE, + HTTP_CLIENT_TIMEOUT + "," + GENERIC_ERROR_MESSAGE, + HTTP_CONFLICT + "," + GENERIC_ERROR_MESSAGE, + HTTP_GONE + "," + GENERIC_ERROR_MESSAGE, + HTTP_LENGTH_REQUIRED + "," + GENERIC_ERROR_MESSAGE, + HTTP_PRECON_FAILED + "," + GENERIC_ERROR_MESSAGE, + HTTP_ENTITY_TOO_LARGE + "," + GENERIC_ERROR_MESSAGE, + HTTP_REQ_TOO_LONG + "," + GENERIC_ERROR_MESSAGE, + HTTP_UNSUPPORTED_TYPE + "," + GENERIC_ERROR_MESSAGE, + HTTP_INTERNAL_ERROR + "," + GENERIC_ERROR_MESSAGE, + HTTP_NOT_IMPLEMENTED + "," + GENERIC_ERROR_MESSAGE, + HTTP_BAD_GATEWAY + "," + GENERIC_ERROR_MESSAGE, + HTTP_GATEWAY_TIMEOUT + "," + GENERIC_ERROR_MESSAGE, + HTTP_VERSION + "," + GENERIC_ERROR_MESSAGE, + + HTTP_NOT_FOUND + "," + "Account '%s' does not exist", + HTTP_UNAVAILABLE + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The engine is not running.", + HTTP_FORBIDDEN + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The operation is not authorized", + HTTP_UNAUTHORIZED + "," + + "Could not query Firebolt at https://test-firebolt.io/web/v3/account/%s/%s. The operation is not authorized" + }) + void testFailedAccountDataRetrieving(int statusCode, String errorMessageTemplate) throws IOException { + injectMockedResponse(httpClient, statusCode, null); + assertErrorMessage(fireboltAccountIdResolver, "one", format(errorMessageTemplate, "one", "resolve")); + assertErrorMessage(fireboltGatewayUrlClient, "two", format(errorMessageTemplate, "two", "engineUrl")); + } + + private void assertErrorMessage(FireboltAccountRetriever accountRetriever, String accountName, String expectedErrorMessagePrefix) { + MatcherAssert.assertThat(Assert.assertThrows(FireboltException.class, () -> accountRetriever.retrieve("access_token", accountName)).getMessage(), Matchers.startsWith(expectedErrorMessagePrefix)); + } + + private void injectMockedResponse(OkHttpClient httpClient, int code, String gatewayResponse) throws IOException { + Response response = mock(Response.class); + Call call = mock(Call.class); + when(httpClient.newCall(any())).thenReturn(call); + when(call.execute()).thenReturn(response); + when(response.code()).thenReturn(code); + if (code == HTTP_OK) { + ResponseBody body = mock(ResponseBody.class); + when(response.body()).thenReturn(body); + when(body.string()).thenReturn(gatewayResponse); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java b/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java index 31ebdf1a..eae589a7 100644 --- a/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java +++ b/src/test/java/com/firebolt/jdbc/client/query/StatementClientImplTest.java @@ -1,41 +1,71 @@ package com.firebolt.jdbc.client.query; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.firebolt.jdbc.client.authentication.FireboltAuthenticationClient; import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.FireboltConnectionTokens; +import com.firebolt.jdbc.connection.UrlUtil; import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.statement.StatementInfoWrapper; import com.firebolt.jdbc.statement.StatementUtil; import lombok.NonNull; import okhttp3.Call; +import okhttp3.Dispatcher; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; +import okhttp3.ResponseBody; import okio.Buffer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.stubbing.Answer; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.net.URISyntaxException; +import java.net.URL; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; +import java.util.Properties; import static com.firebolt.jdbc.client.UserAgentFormatter.userAgent; +import static com.firebolt.jdbc.client.query.StatementClientImpl.HEADER_RESET_SESSION; +import static com.firebolt.jdbc.client.query.StatementClientImpl.HEADER_UPDATE_ENDPOINT; +import static com.firebolt.jdbc.client.query.StatementClientImpl.HEADER_UPDATE_PARAMETER; import static java.lang.String.format; +import static java.util.Optional.ofNullable; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class StatementClientImplTest { + private static final String HOST = "firebolt1"; + private static final FireboltProperties FIREBOLT_PROPERTIES = FireboltProperties.builder().database("db1").compress(true).host("firebolt1").port(555).build(); @Captor private ArgumentCaptor requestArgumentCaptor; @Mock @@ -44,18 +74,25 @@ class StatementClientImplTest { @Mock private FireboltConnection connection; - @Test - void shouldPostSqlQueryWithExpectedUrl() throws FireboltException, IOException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) - .host("firebolt1").port(555).build(); + @ParameterizedTest + @CsvSource({ + "false,http://firebolt1:555/?database=db1&output_format=TabSeparatedWithNamesAndTypes&compress=1&max_execution_time=15", + "true,http://firebolt1:555/?database=db1&account_id=12345&output_format=TabSeparatedWithNamesAndTypes" + }) + void shouldPostSqlQueryWithExpectedUrl(boolean systemEngine, String expectedUrl) throws SQLException, IOException { + assertEquals(expectedUrl, shouldPostSqlQuery(systemEngine).getValue()); + } + + private Entry shouldPostSqlQuery(boolean systemEngine) throws SQLException, IOException { + FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true).host("firebolt1").port(555).accountId("12345").systemEngine(systemEngine).build(); when(connection.getAccessToken()) .thenReturn(Optional.of("token")); - StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, mock(ObjectMapper.class), - "ConnA:1.0.9", "ConnB:2.0.9"); - Call call = getMockedCallWithResponse(200); + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "ConnA:1.0.9", "ConnB:2.0.9"); + injectMockedResponse(okHttpClient, 200, ""); + Call call = getMockedCallWithResponse(200, ""); when(okHttpClient.newCall(any())).thenReturn(call); StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("show databases").get(0); - statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, fireboltProperties.isSystemEngine(), 15, true); + statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, fireboltProperties.isSystemEngine(), 15); verify(okHttpClient).newCall(requestArgumentCaptor.capture()); Request actualRequest = requestArgumentCaptor.getValue(); @@ -63,89 +100,351 @@ void shouldPostSqlQueryWithExpectedUrl() throws FireboltException, IOException { Map expectedHeaders = new LinkedHashMap<>(); expectedHeaders.put("Authorization", "Bearer token"); expectedHeaders.put("User-Agent", userAgent("ConnB/2.0.9 JDBC/%s (Java %s; %s %s; ) ConnA/1.0.9")); - assertEquals(expectedHeaders, extractHeadersMap(actualRequest)); - assertEquals("show databases;", actualQuery); - assertEquals(format( - "http://firebolt1:555/?database=db1&output_format=TabSeparatedWithNamesAndTypes&query_id=%s&compress=1&max_execution_time=15", - statementInfoWrapper.getId()), actualRequest.url().toString()); + //assertEquals("show databases;", actualQuery); + assertSqlStatement("show databases;", actualQuery); + return Map.entry(statementInfoWrapper.getLabel(), actualRequest.url().toString()); } - @Test - void shouldPostSqlQueryForSystemEngine() throws FireboltException, IOException, URISyntaxException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) - .host("firebolt1").port(555).build(); - when(connection.getAccessToken()) - .thenReturn(Optional.of("token")); - StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, mock(ObjectMapper.class), - "ConnA:1.0.9", "ConnB:2.0.9"); - Call call = getMockedCallWithResponse(200); + @ParameterizedTest(name = "infra version:{0}") + @ValueSource(ints = {0, 1, 2}) + void shouldCancelSqlQuery(int infraVersion) throws SQLException, IOException { + when(connection.getInfraVersion()).thenReturn(infraVersion); + String id = "12345"; + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "", ""); + PreparedStatement ps = mock(PreparedStatement.class); + ResultSet rs = mock(ResultSet.class); + when(connection.prepareStatement(anyString())).thenReturn(ps); + when(ps.executeQuery()).thenReturn(rs); + when(rs.next()).thenReturn(true); + when(rs.getString(1)).thenReturn(id); + injectMockedResponse(okHttpClient, 200, ""); + Call call = getMockedCallWithResponse(200, ""); when(okHttpClient.newCall(any())).thenReturn(call); - StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("show databases").get(0); - statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, true, 15, true); - + when(okHttpClient.dispatcher()).thenReturn(mock(Dispatcher.class)); + statementClient.abortStatement(id, FIREBOLT_PROPERTIES); verify(okHttpClient).newCall(requestArgumentCaptor.capture()); - Request actualRequest = requestArgumentCaptor.getValue(); - String actualQuery = getActualRequestString(actualRequest); - - assertEquals("show databases;", actualQuery); - assertEquals("http://firebolt1:555/?output_format=TabSeparatedWithNamesAndTypes", - actualRequest.url().toString()); + assertEquals("http://firebolt1:555/cancel?query_id=12345", + requestArgumentCaptor.getValue().url().uri().toString()); } - @Test - void shouldCancelSqlQuery() throws FireboltException, IOException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) - .host("firebolt1").port(555).build(); - StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, - mock(ObjectMapper.class), "", ""); - Call call = getMockedCallWithResponse(200); + @ParameterizedTest(name = "infra version:{0}") + @ValueSource(ints = {0, 1, 2}) + void shouldIgnoreIfStatementIsNotFoundInDbWhenCancelSqlQuery(int infraVersion) throws SQLException, IOException { + when(connection.getInfraVersion()).thenReturn(infraVersion); + String id = "12345"; + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "", ""); + PreparedStatement ps = mock(PreparedStatement.class); + ResultSet rs = mock(ResultSet.class); + when(connection.prepareStatement(anyString())).thenReturn(ps); + when(ps.executeQuery()).thenReturn(rs); + when(rs.next()).thenReturn(true); + when(rs.getString(1)).thenReturn(id); + injectMockedResponse(okHttpClient, 200, ""); + Call call = getMockedCallWithResponse(400, ""); // BAD REQUEST when(okHttpClient.newCall(any())).thenReturn(call); - statementClient.abortStatement("12345", fireboltProperties); + when(okHttpClient.dispatcher()).thenReturn(mock(Dispatcher.class)); + statementClient.abortStatement(id, FIREBOLT_PROPERTIES); verify(okHttpClient).newCall(requestArgumentCaptor.capture()); assertEquals("http://firebolt1:555/cancel?query_id=12345", requestArgumentCaptor.getValue().url().uri().toString()); } + @ParameterizedTest(name = "infra version:{0}") + @ValueSource(ints = {0, 1, 2}) + void cannotGetStatementIdWhenCancelling(int infraVersion) throws SQLException { + when(connection.getInfraVersion()).thenReturn(infraVersion); + String id = "12345"; + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "", ""); + PreparedStatement ps = mock(PreparedStatement.class); + ResultSet rs = mock(ResultSet.class); + when(connection.prepareStatement(anyString())).thenReturn(ps); + when(ps.executeQuery()).thenReturn(rs); + when(rs.next()).thenReturn(true); + when(rs.getString(1)).thenReturn(null); + when(okHttpClient.dispatcher()).thenReturn(mock(Dispatcher.class)); + assertEquals("Cannot retrieve id for statement with label " + id, assertThrows(FireboltException.class, () -> statementClient.abortStatement(id, FIREBOLT_PROPERTIES)).getMessage()); + } + + @ParameterizedTest + @CsvSource({ + "1,401,The operation is not authorized", + "1,500, Server failed to execute query", + "2,401,The operation is not authorized", + "2,500, Server failed to execute query" + }) + void shouldFailToCancelSqlQuery(int infraVersion, int httpStatus, String errorMessage) throws SQLException, IOException { + when(connection.getInfraVersion()).thenReturn(infraVersion); + String id = "12345"; + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "", ""); + PreparedStatement ps = mock(PreparedStatement.class); + ResultSet rs = mock(ResultSet.class); + when(connection.prepareStatement(anyString())).thenReturn(ps); + when(ps.executeQuery()).thenReturn(rs); + when(rs.next()).thenReturn(true); + when(rs.getString(1)).thenReturn(id); + Call call = getMockedCallWithResponse(httpStatus, ""); + when(okHttpClient.newCall(any())).thenReturn(call); + when(okHttpClient.dispatcher()).thenReturn(mock(Dispatcher.class)); + FireboltException e = assertThrows(FireboltException.class, () -> statementClient.abortStatement(id, FIREBOLT_PROPERTIES)); + assertTrue(e.getMessage().contains(errorMessage)); + } + @Test - void shouldRetryOnUnauthorized() throws IOException, FireboltException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) - .host("firebolt1").port(555).build(); - when(connection.getAccessToken()).thenReturn(Optional.of("oldToken")) - .thenReturn(Optional.of("newToken")); - Call okCall = getMockedCallWithResponse(200); - Call unauthorizedCall = getMockedCallWithResponse(401); + void shouldRetryOnUnauthorized() throws IOException, SQLException { + when(connection.getAccessToken()).thenReturn(Optional.of("oldToken")).thenReturn(Optional.of("newToken")); + Call okCall = getMockedCallWithResponse(200, ""); + Call unauthorizedCall = getMockedCallWithResponse(401, ""); when(okHttpClient.newCall(any())).thenReturn(unauthorizedCall).thenReturn(okCall); - StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, mock(ObjectMapper.class), - "ConnA:1.0.9", "ConnB:2.0.9"); + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "ConnA:1.0.9", "ConnB:2.0.9"); StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("show databases").get(0); - statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, false, 5, true); + statementClient.executeSqlStatement(statementInfoWrapper, FIREBOLT_PROPERTIES, false, 5); verify(okHttpClient, times(2)).newCall(requestArgumentCaptor.capture()); - assertEquals("Bearer oldToken" ,requestArgumentCaptor.getAllValues().get(0).headers().get("Authorization")); - assertEquals("Bearer newToken" ,requestArgumentCaptor.getAllValues().get(1).headers().get("Authorization")); + assertEquals("Bearer oldToken", requestArgumentCaptor.getAllValues().get(0).headers().get("Authorization")); // legit:ignore-secrets + assertEquals("Bearer newToken", requestArgumentCaptor.getAllValues().get(1).headers().get("Authorization")); // legit:ignore-secrets verify(connection).removeExpiredTokens(); } @Test - void shouldNotRetryNoMoreThanOnceOnUnauthorized() throws IOException, FireboltException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db1").compress(true) - .host("firebolt1").port(555).build(); - Call okCall = getMockedCallWithResponse(200); - Call unauthorizedCall = getMockedCallWithResponse(401); + void shouldNotRetryNoMoreThanOnceOnUnauthorized() throws SQLException, IOException { + Call okCall = getMockedCallWithResponse(200, ""); + Call unauthorizedCall = getMockedCallWithResponse(401, ""); when(okHttpClient.newCall(any())).thenReturn(unauthorizedCall).thenReturn(unauthorizedCall).thenReturn(okCall); - StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, mock(ObjectMapper.class), - "ConnA:1.0.9", "ConnB:2.0.9"); + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "ConnA:1.0.9", "ConnB:2.0.9"); StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("show databases").get(0); - FireboltException ex = assertThrows(FireboltException.class, () -> statementClient.executeSqlStatement(statementInfoWrapper, fireboltProperties, false, 5, true)); + FireboltException ex = assertThrows(FireboltException.class, () -> statementClient.executeSqlStatement(statementInfoWrapper, FIREBOLT_PROPERTIES, false, 5)); assertEquals(ExceptionType.UNAUTHORIZED, ex.getType()); verify(okHttpClient, times(2)).newCall(any()); verify(connection, times(2)).removeExpiredTokens(); } - private Call getMockedCallWithResponse(int statusCode) throws IOException { + @ParameterizedTest + @CsvSource({ + "db1,use db1,database=db1,db1", + "db1,use db2,database=db2,db2", + "db1,use db2,,db1", // no header returned + "db1,use db2,database=db3,db3", // use db2 but switched to db3 + + "db1,use database db1,database=db1,db1", + "db1,use database db2,database=db2,db2", + "db1,use database db2,,db1", // no header returned + "db1,use database db2,database=db3,db3" // use db2 but switched to db3 + }) + void useDatabase(String oldDb, String command, String responseHeader, String expectedDb) throws SQLException, IOException { + try (FireboltConnection connection = use("database", oldDb, command, responseHeader)) { + assertEquals(expectedDb, connection.getSessionProperties().getDatabase()); + } + } + + @ParameterizedTest + @CsvSource({ + "e1,use engine e1,engine=e1,e1", + "e1,use engine e2,engine=e2,e2", + "e1,use engine e2,,e1", // no header returned + "e1,use engine e2,engine=e3,e3" // use e2 but switched to e3 + }) + void useEngine(String oldEngine, String command, String responseHeader, String expectedEngine) throws SQLException, IOException { + try (FireboltConnection connection = use("engine", oldEngine, command, responseHeader)) { + assertEquals(expectedEngine, connection.getSessionProperties().getEngine()); + } + } + + @Test + void useChangeSeveralProperties() throws SQLException, IOException { + Properties props = new Properties(); + props.setProperty("database", "db1"); + props.setProperty("engine", "e1"); + props.setProperty("account_id", "a1"); + props.setProperty("compress", "false"); + try (FireboltConnection connection = use(1, props, "does not matter", Map.of( + HEADER_UPDATE_PARAMETER, List.of("database=db2", "engine=e2", "account_id=a1", "addition=something else"), + HEADER_UPDATE_ENDPOINT, List.of("http://other.com?foo=bar")))) { + FireboltProperties fbProps = connection.getSessionProperties(); + assertEquals("db2", fbProps.getDatabase()); + assertEquals("e2", fbProps.getEngine()); + assertEquals("a1", fbProps.getAccountId()); + assertEquals("other.com", connection.getEndpoint()); + Map additionalProperties = fbProps.getAdditionalProperties(); + assertEquals("something else", additionalProperties.get("addition")); + assertEquals("bar", additionalProperties.get("foo")); + } + } + + @ParameterizedTest(name = "infra version:{0}") + @CsvSource({ + "1,https://api.app.firebolt.io/?database=db1&two=second&three=third&compress=1&one=first,https://api.app.firebolt.io/?database=db1&output_format=TabSeparatedWithNamesAndTypes&compress=1", + "2,https://api.app.firebolt.io/?database=db1&account_id=a1&engine=e1&compress=1&one=first&two=second&three=third&query_label=[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12},https://api.app.firebolt.io/?database=db1&account_id=a1&output_format=TabSeparatedWithNamesAndTypes&engine=e1&compress=1&query_label=[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}" + }) + void useResetSession(int infraVersion, String useUrl, String select1Url) throws SQLException, IOException { + Properties props = new Properties(); + props.setProperty("database", "db1"); + props.setProperty("engine", "e1"); + props.setProperty("account_id", "a1"); + props.setProperty("one", "first"); + try (FireboltConnection connection = use(infraVersion, props, "does not matter", Map.of(HEADER_UPDATE_PARAMETER, List.of("two=second")))) { + assertEquals(Map.of("one", "first", "two", "second"), connection.getSessionProperties().getAdditionalProperties()); + + // run set statement + Call setCall = getMockedCallWithResponse(200, ""); + Call select1Call = getMockedCallWithResponse(200, ""); + when(okHttpClient.newCall(any())).thenReturn(setCall, select1Call); + connection.createStatement().executeUpdate("set three=third"); + assertEquals(Map.of("one", "first", "two", "second", "three", "third"), connection.getSessionProperties().getAdditionalProperties()); + + // now reset the session + Call useCall = getMockedCallWithResponse(200, "", Map.of(HEADER_RESET_SESSION, List.of(""))); + + when(okHttpClient.newCall(argThat(new ArgumentMatcher() { + private final String[] useUrls = {useUrl, select1Url}; + private int i = 0; + @Override + public boolean matches(Request request) { + return urlsComparator(useUrls[i++], request.url().url().toString()); + } + }))).thenReturn(useCall, select1Call); + connection.createStatement().executeUpdate("also does not matter"); + // one->first remains here because this is initial property; it should not be removed during session reset + assertEquals(Map.of("one", "first"), connection.getSessionProperties().getAdditionalProperties()); + } + } + + /** + * Compares given URL template with actual URL as following: host and then each query parameter. + * Each string element of template is interpreted as regular expression, each element of actual + * URL as just string. + * @param template + * @param actual + * @return true if matches, false otherwise + */ + private boolean urlsComparator(String template, String actual) { + URL templateUrl = UrlUtil.createUrl(template); + URL actualUrl = UrlUtil.createUrl(actual); + if (!actualUrl.getHost().matches(templateUrl.getHost())) { + return false; + } + Map templateParameters = UrlUtil.getQueryParameters(templateUrl); + Map actualParameters = UrlUtil.getQueryParameters(actualUrl); + if (actualParameters.size() != templateParameters.size()) { + return false; + } + for (Entry actualParameter : actualParameters.entrySet()) { + if (!actualParameter.getValue().matches(templateParameters.get(actualParameter.getKey()))) { + return false; + } + } + return true; + } + + private FireboltConnection use(String propName, String propValue, String command, String responseHeadersStr) throws SQLException, IOException { + Map> responseHeaders = responseHeadersStr == null ? Map.of() : Map.of(HEADER_UPDATE_PARAMETER, List.of(responseHeadersStr.split("\\s*,\\s*"))); + Properties props = new Properties(); + props.setProperty(propName, propValue); + try (FireboltConnection connection = use(1, props, command, responseHeaders)) { + return connection; + } + } + + private FireboltConnection use(int mockedInfraVersion, Properties props, String useCommand, Map> responseHeaders) throws SQLException, IOException { + props.setProperty(FireboltSessionProperty.CONNECTION_TIMEOUT_MILLIS.getKey(), "0"); // simplifies mocking + Call useCall = getMockedCallWithResponse(200, "", responseHeaders); + Call select1Call = getMockedCallWithResponse(200, ""); + when(okHttpClient.newCall(any())).thenReturn(useCall, select1Call); + FireboltConnection connection = new FireboltConnection("url", props, "0") { + { + this.infraVersion = mockedInfraVersion; + try { + connect(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + protected OkHttpClient getHttpClient(FireboltProperties fireboltProperties) { + return okHttpClient; + } + + @Override + protected FireboltAuthenticationClient createFireboltAuthenticationClient(OkHttpClient httpClient) { + FireboltAuthenticationClient client = mock(FireboltAuthenticationClient.class); + try { + lenient().when(client.postConnectionTokens(anyString(), any(), any(), any())).thenReturn(new FireboltConnectionTokens("token", 3600)); + } catch (IOException | SQLException e) { + throw new IllegalStateException(e); + } + return client; + } + + @Override + protected void authenticate() { + sessionProperties = loginProperties; + } + + @Override + protected void assertDatabaseExisting(String database) { + + } + }; + connection.createStatement().executeUpdate(useCommand); + return connection; + } + + @ParameterizedTest + @CsvSource( + value = { + "HTTP status code: 401 Unauthorized, body: {\"error\":\"Authentication token is invalid\",\"code\":16,\"message\":\"Authentication token is invalid\",\"details\":[{\"@type\":\"type.googleapis.com/google.rpc.DebugInfo\",\"stack_entries\":[],\"detail\":\"failed to get user_id from fawkes: entity not found\"}]}; Please associate user with your service account.", + "Engine MyEngine does not exist or not authorized; Please grant at least one role to user associated your service account." + }, + delimiter = ';') + void shouldThrowUnauthorizedExceptionWhenNoAssociatedUser(String serverErrorMessage, String exceptionMessage) throws SQLException, IOException { + when(connection.getAccessToken()).thenReturn(Optional.of("token")); + Call unauthorizedCall = getMockedCallWithResponse(500, serverErrorMessage); + + when(okHttpClient.newCall(any())).thenReturn(unauthorizedCall); + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "ConnA:1.0.9", "ConnB:2.0.9"); + StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("show databases").get(0); + FireboltException exception = assertThrows(FireboltException.class, () -> statementClient.executeSqlStatement(statementInfoWrapper, FIREBOLT_PROPERTIES, false, 5)); + assertEquals(ExceptionType.UNAUTHORIZED, exception.getType()); + assertEquals(format("Could not query Firebolt at %s. %s", HOST, exceptionMessage), exception.getMessage()); + } + + @ParameterizedTest + @CsvSource({ + "java.io.IOException, ERROR", + "okhttp3.internal.http2.StreamResetException, CANCELED", + "java.lang.IllegalArgumentException, ERROR", + }) + void shouldThrowIOException(Class exceptionClass, ExceptionType exceptionType) throws IOException { + Call call = mock(Call.class); + when(call.execute()).thenThrow(exceptionClass); + when(okHttpClient.newCall(any())).thenReturn(call); + StatementClient statementClient = new StatementClientImpl(okHttpClient, connection, "", ""); + StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("select 1").get(0); + FireboltException ex = assertThrows(FireboltException.class, () -> statementClient.executeSqlStatement(statementInfoWrapper, FIREBOLT_PROPERTIES, false, 5)); + assertEquals(exceptionType, ex.getType()); + assertEquals(exceptionClass, ex.getCause().getClass()); + } + + + private Call getMockedCallWithResponse(int statusCode, String content) throws IOException { + return getMockedCallWithResponse(statusCode, content, Map.of()); + } + + private Call getMockedCallWithResponse(int statusCode, String content, Map> responseHeaders) throws IOException { Call call = mock(Call.class); Response response = mock(Response.class); lenient().when(response.code()).thenReturn(statusCode); + ofNullable(responseHeaders).ifPresent(headers -> headers.forEach((key, value) -> lenient().when(response.headers(key)).thenReturn(value))); + lenient().when(response.header(anyString())).then((Answer) invocation -> { + String name = invocation.getArgument(0); + List headers = response.headers(name); + return headers.isEmpty() ? null : headers.get(headers.size() - 1); + }); + + ResponseBody body = mock(ResponseBody.class); + lenient().when(response.body()).thenReturn(body); + lenient().when(body.bytes()).thenReturn(content.getBytes()); + lenient().when(body.byteStream()).thenReturn(new ByteArrayInputStream(content.getBytes())); lenient().when(call.execute()).thenReturn(response); return call; } @@ -156,10 +455,25 @@ private Map extractHeadersMap(Request request) { return headers; } + private void injectMockedResponse(OkHttpClient httpClient, int code, String content) throws IOException { + Response response = mock(Response.class); + Call call = mock(Call.class); + lenient().when(httpClient.newCall(any())).thenReturn(call); + lenient().when(call.execute()).thenReturn(response); + ResponseBody body = mock(ResponseBody.class); + lenient().when(response.body()).thenReturn(body); + lenient().when(body.bytes()).thenReturn(content.getBytes()); + lenient().when(response.code()).thenReturn(code); + } + @NonNull private String getActualRequestString(Request actualRequest) throws IOException { Buffer buffer = new Buffer(); actualRequest.body().writeTo(buffer); return buffer.readUtf8(); } + + private void assertSqlStatement(String expected, String actual) { + assertTrue(actual.matches(expected + "--label:[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}")); + } } \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretTest.java new file mode 100644 index 00000000..4c02fc29 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionServiceSecretTest.java @@ -0,0 +1,118 @@ +package com.firebolt.jdbc.connection; + +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.service.FireboltGatewayUrlService; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Map; +import java.util.Properties; + +import static java.lang.String.format; +import static java.util.stream.Collectors.toMap; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +class FireboltConnectionServiceSecretTest extends FireboltConnectionTest { + private static final String SYSTEM_ENGINE_URL = "jdbc:firebolt:db?env=dev&account=dev"; + + public FireboltConnectionServiceSecretTest() { + super("jdbc:firebolt:db?env=dev&engine=eng&account=dev"); + } + + @Test + void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { + Properties propertiesWithSystemEngine = new Properties(connectionProperties); + try (FireboltConnection fireboltConnection = createConnection(SYSTEM_ENGINE_URL, propertiesWithSystemEngine)) { + fireboltConnection.isValid(500); + verifyNoInteractions(fireboltStatementService); + } + } + + @Test + void shouldNotGetEngineUrlOrDefaultEngineUrlWhenUsingSystemEngine() throws SQLException { + connectionProperties.put("database", "my_db"); + when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); + + try (FireboltConnection connection = createConnection(SYSTEM_ENGINE_URL, connectionProperties)) { + verify(fireboltEngineService, times(0)).getEngine(argThat(props -> "my_db".equals(props.getDatabase()))); + assertEquals("my_endpoint", connection.getSessionProperties().getHost()); + } + } + + @Test + void notExistingDb() throws SQLException { + connectionProperties.put("database", "my_db"); + when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://my_endpoint"); + when(fireboltEngineService.doesDatabaseExist("my_db")).thenReturn(false); + assertEquals("Database my_db does not exist", assertThrows(FireboltException.class, () -> createConnection("jdbc:firebolt:?env=dev&account=dev", connectionProperties)).getMessage()); + } + + @Test + void noAccount() { + assertEquals("Cannot connect: account is missing", assertThrows(FireboltException.class, () -> createConnection("jdbc:firebolt:db", connectionProperties)).getMessage()); + } + + @ParameterizedTest(name = "{0}") + @CsvSource({ + "regular engine,&engine=eng", + "system engine,''" + }) + void getMetadata(String testName, String engineParameter) throws SQLException { + try (FireboltConnection connection = createConnection(format("jdbc:firebolt:db?env=dev&account=dev%s", engineParameter), connectionProperties)) { + DatabaseMetaData dbmd = connection.getMetaData(); + assertFalse(connection.isReadOnly()); + assertFalse(dbmd.isReadOnly()); + assertSame(dbmd, connection.getMetaData()); + connection.close(); + assertThat(assertThrows(SQLException.class, connection::getMetaData).getMessage(), containsString("closed")); + } + } + + @ParameterizedTest(name = "{0}") + @CsvSource({ + "http://the-endpoint,the-endpoint,", + "https://the-endpoint,the-endpoint,", + "the-endpoint,the-endpoint,", + "http://the-endpoint?foo=1&bar=2,the-endpoint,foo=1;bar=2", + "https://the-endpoint?foo=1&bar=2,the-endpoint,foo=1;bar=2", + "the-endpoint?foo=1&bar=2,the-endpoint,foo=1;bar=2", + }) + void checkSystemEngineEndpoint(String gatewayUrl, String expectedHost, String expectedProps) throws SQLException { + @SuppressWarnings("unchecked") FireboltAccountRetriever fireboltGatewayUrlClient = mock(FireboltAccountRetriever.class); + when(fireboltGatewayUrlClient.retrieve(any(), any())).thenReturn(new GatewayUrlResponse(gatewayUrl)); + FireboltGatewayUrlService gatewayUrlService = new FireboltGatewayUrlService(fireboltGatewayUrlClient); + FireboltConnection connection = new FireboltConnectionServiceSecret(SYSTEM_ENGINE_URL, connectionProperties, fireboltAuthenticationService, gatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); + FireboltProperties sessionProperties = connection.getSessionProperties(); + assertEquals(expectedHost, sessionProperties.getHost()); + assertEquals(expectedProps == null ? Map.of() : Arrays.stream(expectedProps.split(";")).map(kv -> kv.split("=")).collect(toMap(kv -> kv[0], kv -> kv[1])), sessionProperties.getAdditionalProperties()); + } + + @Test + void shouldNotFetchTokenNorEngineHostForLocalFirebolt() throws SQLException { + super.shouldNotFetchTokenNorEngineHostForLocalFirebolt(); + verifyNoInteractions(fireboltEngineService); + } + + protected FireboltConnection createConnection(String url, Properties props) throws SQLException { + return new FireboltConnectionServiceSecret(url, props, fireboltAuthenticationService, fireboltGatewayUrlService, fireboltStatementService, fireboltEngineService, fireboltAccountIdService); + } +} diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java index 6cc6f340..c7771052 100644 --- a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionTest.java @@ -1,15 +1,17 @@ package com.firebolt.jdbc.connection; +import com.firebolt.jdbc.CheckedBiFunction; import com.firebolt.jdbc.CheckedFunction; +import com.firebolt.jdbc.client.account.FireboltAccount; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.ExceptionType; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.service.FireboltAccountIdService; import com.firebolt.jdbc.service.FireboltAuthenticationService; -import com.firebolt.jdbc.service.FireboltEngineService; +import com.firebolt.jdbc.service.FireboltEngineInformationSchemaService; +import com.firebolt.jdbc.service.FireboltGatewayUrlService; import com.firebolt.jdbc.service.FireboltStatementService; import com.firebolt.jdbc.statement.StatementInfoWrapper; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -21,11 +23,14 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.MockedStatic; +import org.mockito.MockedConstruction; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.IOException; import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -34,7 +39,10 @@ import java.sql.Savepoint; import java.sql.Statement; import java.sql.Types; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; import java.util.Optional; import java.util.Properties; import java.util.concurrent.Callable; @@ -44,15 +52,21 @@ import java.util.stream.Stream; import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.ACCESS_TOKEN; +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.CLIENT_ID; +import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.CLIENT_SECRET; import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.HOST; -import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.PASSWORD; -import static com.firebolt.jdbc.connection.settings.FireboltSessionProperty.USER; +import static java.sql.Connection.TRANSACTION_READ_COMMITTED; +import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; +import static java.sql.Connection.TRANSACTION_REPEATABLE_READ; +import static java.sql.Connection.TRANSACTION_SERIALIZABLE; import static java.sql.ResultSet.CLOSE_CURSORS_AT_COMMIT; import static java.sql.ResultSet.CONCUR_READ_ONLY; import static java.sql.ResultSet.CONCUR_UPDATABLE; +import static java.sql.ResultSet.HOLD_CURSORS_OVER_COMMIT; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; import static java.sql.ResultSet.TYPE_SCROLL_INSENSITIVE; import static java.sql.ResultSet.TYPE_SCROLL_SENSITIVE; +import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -61,9 +75,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -71,29 +84,35 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -class FireboltConnectionTest { - - private static final String URL = "jdbc:firebolt://api.dev.firebolt.io/db"; - private static final String LOCAL_URL = "jdbc:firebolt://localhost:8123/local_dev_db?ssl=false&max_query_size=10000000&use_standard_sql=1&mask_internal_errors=0&firebolt_enable_beta_functions=1&firebolt_case_insensitive_identifiers=1&rest_api_pull_timeout_sec=3600&rest_api_pull_interval_millisec=5000&rest_api_retry_times=10"; - private final FireboltConnectionTokens fireboltConnectionTokens = FireboltConnectionTokens.builder().build(); +abstract class FireboltConnectionTest { + private static final String LOCAL_URL = "jdbc:firebolt:local_dev_db?account=dev&ssl=false&max_query_size=10000000&mask_internal_errors=0&firebolt_enable_beta_functions=1&firebolt_case_insensitive_identifiers=1&rest_api_pull_timeout_sec=3600&rest_api_pull_interval_millisec=5000&rest_api_retry_times=10&host=localhost"; + private final FireboltConnectionTokens fireboltConnectionTokens = new FireboltConnectionTokens(null, 0); @Captor private ArgumentCaptor propertiesArgumentCaptor; @Captor private ArgumentCaptor queryInfoWrapperArgumentCaptor; @Mock - private FireboltAuthenticationService fireboltAuthenticationService; + protected FireboltAuthenticationService fireboltAuthenticationService; + @Mock + protected FireboltGatewayUrlService fireboltGatewayUrlService; + + @Mock + protected FireboltEngineInformationSchemaService fireboltEngineService; @Mock - private FireboltEngineService fireboltEngineService; + protected FireboltStatementService fireboltStatementService; @Mock - private FireboltStatementService fireboltStatementService; - private Properties connectionProperties = new Properties(); + protected FireboltAccountIdService fireboltAccountIdService; + protected Properties connectionProperties = new Properties(); private static Connection connection; + private final String URL; + + protected FireboltConnectionTest(String url) { + this.URL = url; + } + private static Stream unsupported() { return Stream.of( - Arguments.of("createClob", (Executable) () -> connection.createClob()), - Arguments.of("createNClob", (Executable) () -> connection.createNClob()), - Arguments.of("createBlob", (Executable) () -> connection.createBlob()), Arguments.of("createSQLXML", (Executable) () -> connection.createSQLXML()), Arguments.of("createStruct", (Executable) () -> connection.createStruct("text", new Object[] {"name"})), @@ -105,107 +124,118 @@ private static Stream unsupported() { Arguments.of("prepareStatement(sql, columnIndexes)", (Executable) () -> connection.prepareStatement("select 1", new int[0])), Arguments.of("prepareStatement(sql, columnNames)", (Executable) () -> connection.prepareStatement("select 1", new String[0])), + Arguments.of("setTransactionIsolation", (Executable) () -> connection.setTransactionIsolation(1)), Arguments.of("setSavepoint", (Executable) () -> connection.setSavepoint()), Arguments.of("setSavepoint(name)", (Executable) () -> connection.setSavepoint("select 1")), Arguments.of("releaseSavepoint(savepoint)", (Executable) () -> connection.releaseSavepoint(mock(Savepoint.class))), Arguments.of("setTypeMap", (Executable) () -> connection.setTypeMap(Map.of())), - Arguments.of("rollback(savepoint)", (Executable) () -> connection.rollback(mock(Savepoint.class))), - Arguments.of("nativeSQL", (Executable) () -> connection.nativeSQL("select 1")) + Arguments.of("rollback(savepoint)", (Executable) () -> connection.rollback(mock(Savepoint.class))) ); } private static Stream empty() { return Stream.of( - Arguments.of("getClientInfo", (Callable) () -> connection.getClientInfo(), new Properties()), - Arguments.of("getClientInfo(name)", (Callable) () -> connection.getClientInfo("something"), null), Arguments.of("getTypeMap", (Callable) () -> connection.getTypeMap(), Map.of()), Arguments.of("getWarnings", (Callable) () -> connection.getWarnings(), null) ); } + private Engine engine; + @BeforeEach - void init() throws FireboltException { + void init() throws SQLException { connectionProperties = new Properties(); - connectionProperties.put("user", "user"); - connectionProperties.put("password", "pa$$word"); + connectionProperties.put("client_id", "somebody"); + connectionProperties.put("client_secret", "pa$$word"); connectionProperties.put("compress", "1"); lenient().when(fireboltAuthenticationService.getConnectionTokens(eq("https://api.dev.firebolt.io:443"), any())) .thenReturn(fireboltConnectionTokens); - lenient().when(fireboltEngineService.getEngine(any(), any(), any())).thenReturn(mock(Engine.class)); + lenient().when(fireboltGatewayUrlService.getUrl(any(), any())).thenReturn("http://foo:8080/bar"); + engine = new Engine("endpoint", "id123", "OK", "noname", null); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(engine); + lenient().when(fireboltEngineService.doesDatabaseExist(any())).thenReturn(true); + lenient().when(fireboltAccountIdService.getValue(any(), any())).thenReturn(new FireboltAccount("id", "earth", 1)); } @Test void shouldInitConnection() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertFalse(fireboltConnection.isClosed()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertFalse(fireboltConnection.isClosed()); + } } @Test void shouldNotFetchTokenNorEngineHostForLocalFirebolt() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(LOCAL_URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - verifyNoInteractions(fireboltAuthenticationService); - verifyNoInteractions(fireboltEngineService); - assertFalse(fireboltConnection.isClosed()); + try (FireboltConnection fireboltConnection = createConnection(LOCAL_URL, connectionProperties)) { + verifyNoInteractions(fireboltAuthenticationService); + verifyNoInteractions(fireboltGatewayUrlService); + assertFalse(fireboltConnection.isClosed()); + } } @Test void shouldPrepareStatement() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + shouldPrepareStatement(Connection::prepareStatement); + } + + @Test + void shouldPrepareStatementNoGeneratedKeys() throws SQLException { + shouldPrepareStatement((connection, sql) -> connection.prepareStatement(sql, Statement.NO_GENERATED_KEYS)); + } + + private void shouldPrepareStatement(CheckedBiFunction preparedStatementFactoryMethod) throws SQLException { + when(fireboltStatementService.execute(any(), any(), any())) .thenReturn(Optional.empty()); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - PreparedStatement statement = fireboltConnection - .prepareStatement("INSERT INTO cars(sales, name) VALUES (?, ?)"); - statement.setObject(1, 500); - statement.setObject(2, "Ford"); - statement.execute(); - assertNotNull(fireboltConnection); - assertNotNull(statement); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), any(), anyInt(), anyInt(), - anyBoolean(), any()); - assertEquals("INSERT INTO cars(sales, name) VALUES (500, 'Ford')", - queryInfoWrapperArgumentCaptor.getValue().getSql()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + PreparedStatement statement = preparedStatementFactoryMethod.apply(fireboltConnection, "INSERT INTO cars(sales, name) VALUES (?, ?)"); + statement.setObject(1, 500); + statement.setObject(2, "Ford"); + statement.execute(); + assertNotNull(fireboltConnection); + assertNotNull(statement); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), any(), any()); + assertEquals("INSERT INTO cars(sales, name) VALUES (500, 'Ford')", + queryInfoWrapperArgumentCaptor.getValue().getSql()); + } } @Test void shouldCloseAllStatementsOnClose() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - Statement statement = fireboltConnection.createStatement(); - Statement preparedStatement = fireboltConnection.prepareStatement("test"); - fireboltConnection.close(); - assertTrue(statement.isClosed()); - assertTrue(preparedStatement.isClosed()); - assertTrue(fireboltConnection.isClosed()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + Statement statement = fireboltConnection.createStatement(); + Statement preparedStatement = fireboltConnection.prepareStatement("test"); + fireboltConnection.close(); + assertTrue(statement.isClosed()); + assertTrue(preparedStatement.isClosed()); + assertTrue(fireboltConnection.isClosed()); + } } @Test void createStatement() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertNotNull(fireboltConnection.createStatement()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertNotNull(fireboltConnection.createStatement()); + } } @Test void createStatementWithParameters() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertNotNull(fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertNotNull(fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)); + } } @Test void unsupportedCreateStatementWithParameters() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_READ_ONLY)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE)); - assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_UPDATABLE)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_READ_ONLY)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_INSENSITIVE, CONCUR_UPDATABLE)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_SCROLL_SENSITIVE, CONCUR_UPDATABLE)); + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.createStatement(TYPE_FORWARD_ONLY, CONCUR_UPDATABLE)); + } } @Test @@ -229,20 +259,19 @@ void schema() throws SQLException { } private void validateFlag(CheckedFunction getter, T expected) throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); + FireboltConnection fireboltConnection = createConnection(URL, connectionProperties); assertEquals(expected, getter.apply(fireboltConnection)); fireboltConnection.close(); assertThrows(FireboltException.class, () -> getter.apply(fireboltConnection)); // cannot invoke this method on closed connection } @Test - void prepareCall() throws FireboltException { + void prepareCall() throws SQLException { notSupported(c -> c.prepareCall("select 1")); } @Test - void unsupportedPrepareStatement() throws FireboltException { + void unsupportedPrepareStatement() throws SQLException { notSupported(c -> c.prepareStatement("select 1", ResultSet.TYPE_SCROLL_INSENSITIVE, CONCUR_READ_ONLY)); notSupported(c -> c.prepareStatement("select 1", Statement.RETURN_GENERATED_KEYS)); notSupported(c -> c.prepareStatement("select 1", new int[0])); @@ -251,207 +280,210 @@ void unsupportedPrepareStatement() throws FireboltException { @Test void prepareStatement() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - PreparedStatement ps = fireboltConnection.prepareStatement("select 1", ResultSet.TYPE_FORWARD_ONLY, CONCUR_READ_ONLY); - assertNotNull(ps); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertNotNull(fireboltConnection.prepareStatement("select 1")); + assertNotNull(fireboltConnection.prepareStatement("select 1", ResultSet.TYPE_FORWARD_ONLY, CONCUR_READ_ONLY)); + assertNotNull(fireboltConnection.prepareStatement("select 1", ResultSet.TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, HOLD_CURSORS_OVER_COMMIT)); + } } - private void notSupported(CheckedFunction getter) throws FireboltException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertThrows(SQLFeatureNotSupportedException.class, () -> getter.apply(fireboltConnection)); // cannot invoke this method on closed connection + private void notSupported(CheckedFunction getter) throws SQLException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(SQLFeatureNotSupportedException.class, () -> getter.apply(fireboltConnection)); // cannot invoke this method on closed connection + } } @Test void shouldNotSetNewPropertyWhenConnectionIsNotValidWithTheNewProperty() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenThrow(new FireboltException(ExceptionType.TOO_MANY_REQUESTS)); - assertThrows(FireboltException.class, - () -> fireboltConnection.addProperty(new ImmutablePair<>("custom_1", "1"))); - - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), - propertiesArgumentCaptor.capture(), anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("1", propertiesArgumentCaptor.getValue().getAdditionalProperties().get("custom_1")); - assertEquals("SELECT 1", queryInfoWrapperArgumentCaptor.getValue().getSql()); - assertNull(fireboltConnection.getSessionProperties().getAdditionalProperties().get("custom_1")); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + when(fireboltStatementService.execute(any(), any(), any())) + .thenThrow(new FireboltException(ExceptionType.TOO_MANY_REQUESTS)); + assertThrows(FireboltException.class, + () -> fireboltConnection.addProperty(Map.entry("custom_1", "1"))); + + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), + propertiesArgumentCaptor.capture(), any()); + assertEquals("1", propertiesArgumentCaptor.getValue().getAdditionalProperties().get("custom_1")); + assertEquals("SELECT 1", queryInfoWrapperArgumentCaptor.getValue().getSql()); + assertNull(fireboltConnection.getSessionProperties().getAdditionalProperties().get("custom_1")); + } } @Test void shouldSetNewPropertyWhenConnectionIsValidWithTheNewProperty() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + when(fireboltStatementService.execute(any(), any(), any())) .thenReturn(Optional.empty()); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - Pair newProperties = new ImmutablePair<>("custom_1", "1"); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + Entry newProperties = Map.entry("custom_1", "1"); - fireboltConnection.addProperty(newProperties); + fireboltConnection.addProperty(newProperties); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), - propertiesArgumentCaptor.capture(), anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("1", propertiesArgumentCaptor.getValue().getAdditionalProperties().get("custom_1")); - assertEquals("1", fireboltConnection.getSessionProperties().getAdditionalProperties().get("custom_1")); - assertEquals("SELECT 1", queryInfoWrapperArgumentCaptor.getValue().getSql()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), + propertiesArgumentCaptor.capture(), any()); + assertEquals("1", propertiesArgumentCaptor.getValue().getAdditionalProperties().get("custom_1")); + assertEquals("1", fireboltConnection.getSessionProperties().getAdditionalProperties().get("custom_1")); + assertEquals(List.of("SELECT 1"), queryInfoWrapperArgumentCaptor.getAllValues().stream().map(StatementInfoWrapper::getSql).collect(toList())); + } } @Test void shouldValidateConnectionWhenCallingIsValid() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + when(fireboltStatementService.execute(any(), any(), any())) .thenReturn(Optional.empty()); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - fireboltConnection.isValid(500); - - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), - propertiesArgumentCaptor.capture(), anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("SELECT 1", queryInfoWrapperArgumentCaptor.getValue().getSql()); - } - - @Test - void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { - Properties propertiesWithSystemEngine = new Properties(connectionProperties); - propertiesWithSystemEngine.put("engine_name", "system"); - FireboltConnection fireboltConnection = new FireboltConnection(URL, propertiesWithSystemEngine, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - fireboltConnection.isValid(500); - - verifyNoInteractions(fireboltStatementService); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + fireboltConnection.isValid(500); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), + propertiesArgumentCaptor.capture(), any()); + assertEquals(List.of("SELECT 1"), queryInfoWrapperArgumentCaptor.getAllValues().stream().map(StatementInfoWrapper::getSql).collect(toList())); + } } @Test void shouldIgnore429WhenValidatingConnection() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + when(fireboltStatementService.execute(any(), any(), any())) .thenThrow(new FireboltException(ExceptionType.TOO_MANY_REQUESTS)); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertTrue(fireboltConnection.isValid(500)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertTrue(fireboltConnection.isValid(500)); + } } @Test void shouldReturnFalseWhenValidatingConnectionThrowsAnException() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + when(fireboltStatementService.execute(any(), any(), any())) .thenThrow(new FireboltException(ExceptionType.ERROR)); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertFalse(fireboltConnection.isValid(500)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertFalse(fireboltConnection.isValid(500)); + } } @Test void shouldThrowExceptionWhenValidatingConnectionWithNegativeTimeout() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertThrows(FireboltException.class, () -> fireboltConnection.isValid(-1)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(FireboltException.class, () -> fireboltConnection.isValid(-1)); + } } @Test void shouldReturnFalseWhenValidatingClosedConnection() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - fireboltConnection.close(); - assertFalse(fireboltConnection.isValid(50)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + fireboltConnection.close(); + assertFalse(fireboltConnection.isValid(50)); + } } @Test void shouldExtractConnectorOverrides() throws SQLException { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) + when(fireboltStatementService.execute(any(), any(), any())) .thenReturn(Optional.empty()); connectionProperties.put("user_clients", "ConnA:1.0.9,ConnB:2.8.0"); connectionProperties.put("user_drivers", "DriverA:2.0.9,DriverB:3.8.0"); - FireboltConnection fireboltConnectionImpl = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - - PreparedStatement statement = fireboltConnectionImpl.prepareStatement("SELECT 1"); - statement.execute(); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + PreparedStatement statement = fireboltConnection.prepareStatement("SELECT 1"); + statement.execute(); - verify(fireboltStatementService).execute(any(), propertiesArgumentCaptor.capture(), anyInt(), anyInt(), - anyBoolean(), any()); - assertNull(propertiesArgumentCaptor.getValue().getAdditionalProperties().get("user_clients")); - assertNull(propertiesArgumentCaptor.getValue().getAdditionalProperties().get("user_drivers")); - assertNull(fireboltConnectionImpl.getSessionProperties().getAdditionalProperties().get("user_clients")); - assertNull(fireboltConnectionImpl.getSessionProperties().getAdditionalProperties().get("user_drivers")); + verify(fireboltStatementService).execute(any(), propertiesArgumentCaptor.capture(), any()); + assertNull(propertiesArgumentCaptor.getValue().getAdditionalProperties().get("user_clients")); + assertNull(propertiesArgumentCaptor.getValue().getAdditionalProperties().get("user_drivers")); + assertNull(fireboltConnection.getSessionProperties().getAdditionalProperties().get("user_clients")); + assertNull(fireboltConnection.getSessionProperties().getAdditionalProperties().get("user_drivers")); + } } @Test void shouldGetEngineNameFromHost() throws SQLException { - when(fireboltEngineService.getEngineNameFromHost(any())).thenReturn("myHost_345"); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals("myHost_345", fireboltConnection.getEngine()); + connectionProperties.put("engine", "hello"); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(engine.getName(), fireboltConnection.getEngine()); + } } @Test void shouldInitNetworkTimeoutWithPropertyByDefault() throws SQLException { connectionProperties.put("socket_timeout_millis", "60"); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals(60, fireboltConnection.getNetworkTimeout()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(60, fireboltConnection.getNetworkTimeout()); + } } @Test void shouldInitConnectionTimeoutWithPropertyByDefault() throws SQLException { connectionProperties.put("connection_timeout_millis", "50"); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals(50, fireboltConnection.getConnectionTimeout()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(50, fireboltConnection.getConnectionTimeout()); + } } @Test void shouldCloseConnectionWhenAbortingConnection() throws SQLException, InterruptedException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - ExecutorService executorService = Executors.newFixedThreadPool(10); - fireboltConnection.abort(executorService); - executorService.awaitTermination(1, TimeUnit.SECONDS); - assertTrue(fireboltConnection.isClosed()); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + ExecutorService executorService = Executors.newFixedThreadPool(10); + fireboltConnection.abort(executorService); + assertFalse(executorService.awaitTermination(1, TimeUnit.SECONDS)); + assertTrue(fireboltConnection.isClosed()); + } + } + + @Test + void shouldThrowExceptionIfAbortingWithNullExecutor() throws SQLException, InterruptedException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(FireboltException.class, () -> fireboltConnection.abort(null)); + } } @Test void shouldRemoveExpiredToken() throws SQLException { - FireboltProperties fireboltProperties = FireboltProperties.builder().host("host").path("/db").port(8080) - .build(); - try (MockedStatic mockedFireboltProperties = Mockito.mockStatic(FireboltProperties.class)) { - when(FireboltProperties.of(any())).thenReturn(fireboltProperties); - when(fireboltAuthenticationService.getConnectionTokens("http://host:8080", fireboltProperties)) - .thenReturn(FireboltConnectionTokens.builder().build()); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - fireboltConnection.removeExpiredTokens(); - verify(fireboltAuthenticationService).removeConnectionTokens("http://host:8080", fireboltProperties); + FireboltProperties fireboltProperties = FireboltProperties.builder().host("host").database("db").port(8080).account("dev").build(); + String url = fireboltProperties.getHttpConnectionUrl(); + try (MockedConstruction mockedFireboltPropertiesConstruction = Mockito.mockConstruction(FireboltProperties.class, (fireboltPropertiesMock, context) -> { + when(fireboltPropertiesMock.getAccount()).thenReturn(fireboltProperties.getAccount()); + when(fireboltPropertiesMock.getDatabase()).thenReturn(fireboltProperties.getDatabase()); + when(fireboltPropertiesMock.getHttpConnectionUrl()).thenReturn(fireboltProperties.getHttpConnectionUrl()); + when(fireboltPropertiesMock.toBuilder()).thenReturn(fireboltProperties.toBuilder()); + lenient().when(fireboltAuthenticationService.getConnectionTokens(eq(url), argThat(argument -> true))).thenReturn(new FireboltConnectionTokens(null, 0)); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://hello", null, null, null, null)); + })) { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + fireboltConnection.removeExpiredTokens(); + verify(fireboltAuthenticationService).removeConnectionTokens(eq(url), argThat(argument -> true)); + } } } @Test void shouldReturnConnectionTokenWhenAvailable() throws SQLException { String accessToken = "hello"; - FireboltProperties fireboltProperties = FireboltProperties.builder().host("host").path("/db").port(8080) - .build(); - try (MockedStatic mockedFireboltProperties = Mockito.mockStatic(FireboltProperties.class)) { - when(FireboltProperties.of(any())).thenReturn(fireboltProperties); - FireboltConnectionTokens connectionTokens = FireboltConnectionTokens.builder().accessToken(accessToken).build(); - when(fireboltAuthenticationService.getConnectionTokens(eq("http://host:8080"), any())) - .thenReturn(connectionTokens); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - verify(fireboltAuthenticationService).getConnectionTokens("http://host:8080", fireboltProperties); - assertEquals(accessToken, fireboltConnection.getAccessToken().get()); + FireboltProperties fireboltProperties = FireboltProperties.builder().host("host").database("db").port(8080).account("dev").build(); + String url = fireboltProperties.getHttpConnectionUrl(); + + try (MockedConstruction mockedFireboltPropertiesConstruction = Mockito.mockConstruction(FireboltProperties.class, (fireboltPropertiesMock, context) -> { + when(fireboltPropertiesMock.getAccount()).thenReturn(fireboltProperties.getAccount()); + when(fireboltPropertiesMock.getHttpConnectionUrl()).thenReturn(url); + when(fireboltPropertiesMock.getDatabase()).thenReturn(fireboltProperties.getDatabase()); + when(fireboltPropertiesMock.toBuilder()).thenReturn(fireboltProperties.toBuilder()); + })) { + FireboltConnectionTokens connectionTokens = new FireboltConnectionTokens(accessToken, 0); + when(fireboltAuthenticationService.getConnectionTokens(eq(url), any())).thenReturn(connectionTokens); + lenient().when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://engineHost", null, null, null, null)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + verify(fireboltAuthenticationService).getConnectionTokens(eq(url), argThat(argument -> Objects.equals(argument.getHttpConnectionUrl(), url))); + assertEquals(accessToken, fireboltConnection.getAccessToken().get()); + } } } @Test void shouldNotReturnConnectionTokenWithLocalDb() throws SQLException { - FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").path("/db").port(8080) - .build(); - try (MockedStatic mockedFireboltProperties = Mockito.mockStatic(FireboltProperties.class)) { - when(FireboltProperties.of(any())).thenReturn(fireboltProperties); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals(Optional.empty(), fireboltConnection.getAccessToken()); - verifyNoInteractions(fireboltAuthenticationService); + FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").build(); + try (MockedConstruction mockedFireboltPropertiesConstruction = Mockito.mockConstruction(FireboltProperties.class, (fireboltPropertiesMock, context) -> { + when(fireboltPropertiesMock.getHost()).thenReturn(fireboltProperties.getHost()); + when(fireboltPropertiesMock.toBuilder()).thenReturn(fireboltProperties.toBuilder()); + })) { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(Optional.empty(), fireboltConnection.getAccessToken()); + verifyNoInteractions(fireboltAuthenticationService); + } } } @@ -468,9 +500,8 @@ void shouldGetConnectionTokenFromProperties(String host, String configuredAccess if (configuredAccessToken != null) { propsWithToken.setProperty(ACCESS_TOKEN.getKey(), configuredAccessToken); } - try (FireboltConnection connection = new FireboltConnection(URL, propsWithToken, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { - assertEquals(expectedAccessToken, connection.getAccessToken().orElse(null)); + try (FireboltConnection fireboltConnection = createConnection(URL, propsWithToken)) { + assertEquals(expectedAccessToken, fireboltConnection.getAccessToken().orElse(null)); Mockito.verifyNoMoreInteractions(fireboltAuthenticationService); } } @@ -479,115 +510,188 @@ void shouldGetConnectionTokenFromProperties(String host, String configuredAccess void shouldThrowExceptionIfBothAccessTokenAndUserPasswordAreSupplied() { Properties propsWithToken = new Properties(); propsWithToken.setProperty(ACCESS_TOKEN.getKey(), "my-token"); - propsWithToken.setProperty(USER.getKey(), "my-user"); - propsWithToken.setProperty(PASSWORD.getKey(), "my-password"); - assertThrows(SQLException.class, () -> new FireboltConnection(URL, propsWithToken, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)); + propsWithToken.setProperty(CLIENT_ID.getKey(), "my-client"); + propsWithToken.setProperty(CLIENT_SECRET.getKey(), "my-secret"); + assertThrows(SQLException.class, () -> createConnection(URL, propsWithToken)); } @Test void shouldSetNetworkTimeout() throws SQLException { - FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").path("/db") - .socketTimeoutMillis(5).port(8080).build(); - try (MockedStatic mockedFireboltProperties = Mockito.mockStatic(FireboltProperties.class)) { - when(FireboltProperties.of(any())).thenReturn(fireboltProperties); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals(5, fireboltConnection.getNetworkTimeout()); - fireboltConnection.setNetworkTimeout(null, 1); - assertEquals(1, fireboltConnection.getNetworkTimeout()); + FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").socketTimeoutMillis(5).build(); + try (MockedConstruction mockedFireboltPropertiesConstruction = Mockito.mockConstruction(FireboltProperties.class, (fireboltPropertiesMock, context) -> { + when(fireboltPropertiesMock.getHost()).thenReturn(fireboltProperties.getHost()); + when(fireboltPropertiesMock.getSocketTimeoutMillis()).thenReturn(fireboltProperties.getSocketTimeoutMillis()); + when(fireboltPropertiesMock.toBuilder()).thenReturn(fireboltProperties.toBuilder()); + })) { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(5, fireboltConnection.getNetworkTimeout()); + fireboltConnection.setNetworkTimeout(null, 1); + assertEquals(1, fireboltConnection.getNetworkTimeout()); + } } } @Test void shouldUseConnectionTimeoutFromProperties() throws SQLException { - FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").path("/db") - .connectionTimeoutMillis(20).port(8080).build(); - try (MockedStatic mockedFireboltProperties = Mockito.mockStatic(FireboltProperties.class)) { - when(FireboltProperties.of(any())).thenReturn(fireboltProperties); - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - assertEquals(20, fireboltConnection.getConnectionTimeout()); + FireboltProperties fireboltProperties = FireboltProperties.builder().host("localhost").connectionTimeoutMillis(20).build(); + try (MockedConstruction mockedFireboltPropertiesConstruction = Mockito.mockConstruction(FireboltProperties.class, (fireboltPropertiesMock, context) -> { + when(fireboltPropertiesMock.getHost()).thenReturn(fireboltProperties.getHost()); + when(fireboltPropertiesMock.getConnectionTimeoutMillis()).thenReturn(fireboltProperties.getConnectionTimeoutMillis()); + when(fireboltPropertiesMock.toBuilder()).thenReturn(fireboltProperties.toBuilder()); + })) { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(fireboltProperties.getConnectionTimeoutMillis(), fireboltConnection.getConnectionTimeout()); + } } } @Test void shouldThrowExceptionWhenTryingToUseClosedConnection() throws SQLException { - FireboltConnection fireboltConnection = new FireboltConnection(URL, connectionProperties, - fireboltAuthenticationService, fireboltEngineService, fireboltStatementService); - fireboltConnection.close(); - assertThrows(FireboltException.class, fireboltConnection::getCatalog); + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + fireboltConnection.close(); + assertThrows(FireboltException.class, fireboltConnection::getCatalog); + } } @Test void shouldUnwrapFireboltConnection() throws SQLException { - Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService); - assertTrue(connection.isWrapperFor(FireboltConnection.class)); - assertEquals(connection, connection.unwrap(FireboltConnection.class)); + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + assertTrue(fireboltConnection.isWrapperFor(FireboltConnection.class)); + assertEquals(fireboltConnection, fireboltConnection.unwrap(FireboltConnection.class)); + } } @Test void shouldThrowExceptionWhenCannotUnwrap() throws SQLException { - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { - assertFalse(connection.isWrapperFor(String.class)); - assertThrows(SQLException.class, () -> connection.unwrap(String.class)); + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + assertFalse(fireboltConnection.isWrapperFor(String.class)); + assertThrows(SQLException.class, () -> fireboltConnection.unwrap(String.class)); } } @Test void shouldGetDatabaseWhenGettingCatalog() throws SQLException { connectionProperties.put("database", "db"); - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { - assertEquals("db", connection.getCatalog()); + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals("noname", fireboltConnection.getCatalog()); // retrieved engine's DB's name is "noname". Firebolt treats DB as catalog } } @Test void shouldGetNoneTransactionIsolation() throws SQLException { connectionProperties.put("database", "db"); - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { - assertEquals(Connection.TRANSACTION_NONE, connection.getTransactionIsolation()); + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals(Connection.TRANSACTION_NONE, fireboltConnection.getTransactionIsolation()); + fireboltConnection.setTransactionIsolation(Connection.TRANSACTION_NONE); // should work + assertEquals(Connection.TRANSACTION_NONE, fireboltConnection.getTransactionIsolation()); + for (int transactionIsolation : new int [] {TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_READ, TRANSACTION_SERIALIZABLE}) { + assertThrows(SQLFeatureNotSupportedException.class, () -> fireboltConnection.setTransactionIsolation(transactionIsolation)); + } + // despite the failed attempts to change transaction isolation to unsupported value it remains TRANSACTION_NONE + assertEquals(Connection.TRANSACTION_NONE, fireboltConnection.getTransactionIsolation()); } } @Test void shouldThrowExceptionWhenPreparingStatementWIthInvalidResultSetType() throws SQLException { connectionProperties.put("database", "db"); - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { assertThrows(SQLFeatureNotSupportedException.class, - () -> connection.prepareStatement("any", TYPE_SCROLL_INSENSITIVE, 0)); + () -> fireboltConnection.prepareStatement("any", TYPE_SCROLL_INSENSITIVE, 0)); } } @Test void createArray() throws SQLException { - try (Connection connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService)) { + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { Object[] data = new Object[] {"red", "green", "blue"}; - Array array = connection.createArrayOf("text", data); + Array array = fireboltConnection.createArrayOf("text", data); assertEquals(Types.VARCHAR, array.getBaseType()); assertArrayEquals(data, (Object[])array.getArray()); } } + @Test + void createBlob() throws SQLException, IOException { + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + Blob blob = fireboltConnection.createBlob(); + String str = "hello"; + blob.setBytes(1, str.getBytes()); + assertEquals(str, new String(blob.getBinaryStream().readAllBytes())); + } + } + + @Test + void createClob() throws SQLException, IOException { + try (Connection fireboltConnection = createConnection(URL, connectionProperties)) { + Clob clob = fireboltConnection.createClob(); + String str = "hello"; + clob.setString(1, str); + assertEquals(str, new String(clob.getAsciiStream().readAllBytes())); + } + } + @ParameterizedTest(name = "{0}") @MethodSource("unsupported") - void shouldThrowSQLFeatureNotSupportedException(String name, Executable function) throws FireboltException { - connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService); + void shouldThrowSQLFeatureNotSupportedException(String name, Executable function) throws SQLException { + connection = createConnection(URL, connectionProperties); assertThrows(SQLFeatureNotSupportedException.class, function); } @ParameterizedTest(name = "{0}") @MethodSource("empty") void shouldReturnEmptyResult(String name, Callable function, Object expected) throws Exception { - connection = new FireboltConnection(URL, connectionProperties, fireboltAuthenticationService, - fireboltEngineService, fireboltStatementService); + connection = createConnection(URL, connectionProperties); assertEquals(expected, function.call()); } + + @Test + void shouldGetEngineUrlWhenEngineIsProvided() throws SQLException { + connectionProperties.put("engine", "engine"); + when(fireboltEngineService.getEngine(any())).thenReturn(new Engine("http://my_endpoint", null, null, null, null)); + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + verify(fireboltEngineService).getEngine(argThat(props -> "engine".equals(props.getEngine()) && "db".equals(props.getDatabase()))); + assertEquals("http://my_endpoint", fireboltConnection.getSessionProperties().getHost()); + } + } + + @Test + void nativeSql() throws SQLException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertEquals("SELECT 1", fireboltConnection.nativeSQL("SELECT 1")); + } + } + + @Test + void unsupportedNativeSql() throws SQLException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + assertThrows(SQLException.class, () -> fireboltConnection.nativeSQL("SELECT {d '2001-01-01'} FROM TEST")); + } + } + + @Test + void getClientInfo() throws SQLException { + try (FireboltConnection fireboltConnection = createConnection(URL, connectionProperties)) { + Properties info = fireboltConnection.getClientInfo(); + // from URL + assertEquals("dev", info.getProperty("environment")); + assertEquals("dev", fireboltConnection.getClientInfo("environment")); // key + assertEquals("dev", fireboltConnection.getClientInfo("env")); // alias + + // from connectionProperties + assertEquals("somebody", info.getProperty("client_id")); + assertEquals("somebody", fireboltConnection.getClientInfo("client_id")); // key + assertEquals("somebody", fireboltConnection.getClientInfo("user")); // alias + + // default value + assertEquals("60", info.getProperty("tcp_keep_idle")); + assertEquals("60", fireboltConnection.getClientInfo("tcp_keep_idle")); + + // deprecated - should not appear + assertFalse(info.containsKey("time_to_live_millis")); + assertNull(fireboltConnection.getClientInfo("time_to_live_millis")); + } + } + + protected abstract FireboltConnection createConnection(String url, Properties props) throws SQLException; } diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordTest.java new file mode 100644 index 00000000..927fcdf3 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/connection/FireboltConnectionUserPasswordTest.java @@ -0,0 +1,78 @@ +package com.firebolt.jdbc.connection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; +import java.util.Properties; + +import static com.firebolt.jdbc.connection.FireboltConnectionUserPassword.SYSTEM_ENGINE_NAME; +import static java.lang.String.format; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +class FireboltConnectionUserPasswordTest extends FireboltConnectionTest { + private static final String SYSTEM_ENGINE_URL = "jdbc:firebolt:db?env=dev&account=dev&engine=system"; + + public FireboltConnectionUserPasswordTest() { + super("jdbc:firebolt://api.dev.firebolt.io/db"); + } + + @Test + void shouldNotValidateConnectionWhenCallingIsValidWhenUsingSystemEngine() throws SQLException { + Properties propertiesWithSystemEngine = new Properties(connectionProperties); + try (FireboltConnection fireboltConnection = createConnection(SYSTEM_ENGINE_URL, propertiesWithSystemEngine)) { + fireboltConnection.isValid(500); + verifyNoInteractions(fireboltStatementService); + } + } + + @Test + void shouldNotGetEngineUrlOrDefaultEngineUrlWhenUsingSystemEngine() throws SQLException { + connectionProperties.put("database", "my_db"); + try (FireboltConnection connection = createConnection(SYSTEM_ENGINE_URL, connectionProperties)) { + verify(fireboltEngineService, times(1)).getEngine(argThat(props -> "my_db".equals(props.getDatabase()) && SYSTEM_ENGINE_NAME.equals(props.getEngine()))); + assertEquals("endpoint", connection.getSessionProperties().getHost()); + } + } + + @Test + void noEngineAndDb() throws SQLException { + try (FireboltConnection connection = createConnection("jdbc:firebolt://api.dev.firebolt.io", connectionProperties)) { + assertEquals("endpoint", connection.getSessionProperties().getHost()); + assertNotNull(connection.getSessionProperties().getEngine()); + assertFalse(connection.getSessionProperties().isSystemEngine()); + } + } + + @ParameterizedTest + @CsvSource({ + "eng", + "system" + }) + void getMetadata(String engine) throws SQLException { + try (FireboltConnection connection = createConnection(format("jdbc:firebolt:db?env=dev&engine=%s&account=dev", engine), connectionProperties)) { + DatabaseMetaData dbmd = connection.getMetaData(); + assertFalse(connection.isReadOnly()); + assertFalse(dbmd.isReadOnly()); + assertSame(dbmd, connection.getMetaData()); + connection.close(); + assertThat(assertThrows(SQLException.class, connection::getMetaData).getMessage(), containsString("closed")); + } + } + + protected FireboltConnection createConnection(String url, Properties props) throws SQLException { + return new FireboltConnectionUserPassword(url, props, fireboltAuthenticationService, fireboltStatementService, fireboltEngineService); + } +} diff --git a/src/test/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtilTest.java b/src/test/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtilTest.java deleted file mode 100644 index 598a6429..00000000 --- a/src/test/java/com/firebolt/jdbc/connection/FireboltJdbcUrlUtilTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.firebolt.jdbc.connection; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Properties; - -import org.junit.jupiter.api.Test; - -class FireboltJdbcUrlUtilTest { - - @Test - void shouldGetAllPropertiesFromUri() { - String uri = "jdbc:firebolt://api.dev.firebolt.io:123/Tutorial_11_05?use_standard_sql=0&account=firebolt"; - Properties properties = FireboltJdbcUrlUtil.extractProperties(uri); - - Properties expectedProperties = new Properties(); - expectedProperties.put("path", "/Tutorial_11_05"); - expectedProperties.put("host", "api.dev.firebolt.io"); - expectedProperties.put("port", "123"); - expectedProperties.put("use_standard_sql", "0"); - expectedProperties.put("account", "firebolt"); - - assertEquals(expectedProperties, properties); - } -} diff --git a/src/test/java/com/firebolt/jdbc/connection/UrlUtilTest.java b/src/test/java/com/firebolt/jdbc/connection/UrlUtilTest.java new file mode 100644 index 00000000..10b4ea22 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/connection/UrlUtilTest.java @@ -0,0 +1,63 @@ +package com.firebolt.jdbc.connection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class UrlUtilTest { + + + @ParameterizedTest + @CsvSource({ + "jdbc:firebolt:Tutorial_11_05/?host=api.dev.firebolt.io&account=firebolt, Tutorial_11_05, api.dev.firebolt.io, firebolt", + "jdbc:firebolt:Tutorial_11_05?host=api.dev.firebolt.io&account=firebolt, Tutorial_11_05, api.dev.firebolt.io, firebolt", + "jdbc:firebolt:/?&host=api.dev.firebolt.io&account=firebolt, '', api.dev.firebolt.io, firebolt" + }) + void shouldGetAllPropertiesFromUri(String uri, String expectedPath, String expectedHost, String expectedAccount) { + Properties properties = UrlUtil.extractProperties(uri); + Properties expectedProperties = new Properties(); + expectedProperties.put("path", expectedPath); + expectedProperties.put("host", expectedHost); + expectedProperties.put("account", expectedAccount); + assertEquals(expectedProperties, properties); + } + + @Test + void createUrl() throws MalformedURLException { + String spec = "http://myhost/path?x=1&y=2"; + assertEquals(new URL(spec), UrlUtil.createUrl(spec)); + } + + @Test + void createBadUrl() { + assertEquals(MalformedURLException.class, assertThrows(IllegalArgumentException.class, () -> UrlUtil.createUrl("not url")).getCause().getClass()); + } + + @ParameterizedTest + @ValueSource(strings = {"http://host", "http://host/", "http://host/?", "http://host?", "http://host:8080", "http://host:8080/", "http://host:8080/?", "http://host:8080?"}) + void getQueryParametersNoParameters(String spec) throws MalformedURLException { + assertEquals(Map.of(), UrlUtil.getQueryParameters(new URL(spec))); + } + + @ParameterizedTest + @ValueSource(strings = { + "http://the-host.com?database&engine=diesel&format=json", // set each parameter only once + "http://the-host.com?database&format=xml&engine=benzine&engine=diesel&format=json" // override parameters + }) + void getQueryParameters() throws MalformedURLException { + Map expected = new HashMap<>(); + expected.put("engine", "diesel"); + expected.put("format", "json"); + assertEquals(expected, UrlUtil.getQueryParameters(new URL("http://the-host.com?database&engine=diesel&format=json"))); + } +} diff --git a/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java b/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java index 8edc77e9..bb015f6a 100644 --- a/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java +++ b/src/test/java/com/firebolt/jdbc/connection/settings/FireboltPropertiesTest.java @@ -1,34 +1,43 @@ package com.firebolt.jdbc.connection.settings; -import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import java.util.HashMap; import java.util.Map; import java.util.Properties; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class FireboltPropertiesTest { @Test void shouldHaveDefaultPropertiesWhenOnlyTheRequiredFieldsAreSpecified() { - FireboltProperties expectedDefaultProperties = FireboltProperties.builder().database("db").bufferSize(65536) - .sslCertificatePath("").sslMode("strict").path("/").port(443) // 443 by default as SSL is enabled by - // default - .compress(true).user(null).password(null).host("host").ssl(true).additionalProperties(new HashMap<>()) - .account(null).engine(null).keepAliveTimeoutMillis(300000).maxConnectionsTotal(300).maxRetries(3) - .socketTimeoutMillis(0).connectionTimeoutMillis(60000).tcpKeepInterval(30).tcpKeepIdle(60) + FireboltProperties expectedDefaultProperties = FireboltProperties.builder().engine("engine").database("db").bufferSize(65536) + .sslCertificatePath("").sslMode("strict").path("").port(443) // 443 by default as SSL is enabled by + .systemEngine(false).compress(true) // default + .principal(null).secret(null).host("host").ssl(true).initialAdditionalProperties(new HashMap<>()) + .keepAliveTimeoutMillis(300000).maxConnectionsTotal(300).maxRetries(3) + .socketTimeoutMillis(0).connectionTimeoutMillis(60000).tcpKeepInterval(30).environment("app").tcpKeepIdle(60) .tcpKeepCount(10).build(); Properties properties = new Properties(); + properties.put("engine", "engine"); properties.put("host", "host"); properties.put("database", "db"); - assertEquals(expectedDefaultProperties, FireboltProperties.of(properties)); + assertEquals(expectedDefaultProperties, new FireboltProperties(properties)); } @Test void shouldHaveAllTheSpecifiedCustomProperties() { Properties properties = new Properties(); + properties.put("engine", "my_test"); properties.put("buffer_size", "51"); properties.put("socket_timeout_millis", "20"); properties.put("ssl", "1"); @@ -36,95 +45,127 @@ void shouldHaveAllTheSpecifiedCustomProperties() { properties.put("database", "myDb"); properties.put("ssl_certificate_path", "root_cert"); properties.put("ssl_mode", "none"); - properties.put("path", "/example"); + properties.put("path", "example"); properties.put("someCustomProperties", "custom_value"); properties.put("compress", "1"); Map customProperties = new HashMap<>(); customProperties.put("someCustomProperties", "custom_value"); - FireboltProperties expectedDefaultProperties = FireboltProperties.builder().bufferSize(51) - .sslCertificatePath("root_cert").sslMode("none").path("/example").database("myDb").compress(true) - .port(443).user(null).password(null).host("myDummyHost").ssl(true) - .additionalProperties(customProperties).account(null).engine(null).keepAliveTimeoutMillis(300000) + FireboltProperties expectedDefaultProperties = FireboltProperties.builder().engine("my_test").bufferSize(51) + .sslCertificatePath("root_cert").sslMode("none").path("example").database("myDb").compress(true) + .port(443).principal(null).secret(null).host("myDummyHost").ssl(true).systemEngine(false) + .initialAdditionalProperties(customProperties).keepAliveTimeoutMillis(300000) .maxConnectionsTotal(300).maxRetries(3).socketTimeoutMillis(20).connectionTimeoutMillis(60000) - .tcpKeepInterval(30).tcpKeepIdle(60).tcpKeepCount(10).build(); - assertEquals(expectedDefaultProperties, FireboltProperties.of(properties)); + .tcpKeepInterval(30).tcpKeepIdle(60).tcpKeepCount(10).environment("app").build(); + assertEquals(expectedDefaultProperties, new FireboltProperties(properties)); } @Test - void shouldUsePathParamAsDb() { - Properties properties = new Properties(); - properties.put("path", "/example"); - properties.put("host", "host"); - - assertEquals("example", FireboltProperties.of(properties).getDatabase()); + void shouldAddAdditionalProperties() { + FireboltProperties props = new FireboltProperties(new Properties()); + assertTrue(props.getAdditionalProperties().isEmpty()); + props.addProperty(Map.entry("a", "1")); + props.addProperty("b", "2"); + assertEquals(Map.of("a", "1", "b", "2"), props.getAdditionalProperties()); } @Test - void shouldSupportBooleansForBooleanProperties() { + void updateSpecialProperties() { Properties properties = new Properties(); - properties.put("path", "/example"); - properties.put("host", "host"); - properties.put("ssl", "true"); - properties.put("compress", "false"); - - assertTrue(FireboltProperties.of(properties).isSsl()); - assertFalse(FireboltProperties.of(properties).isCompress()); + properties.put("database", "db1"); + properties.put("engine", "e1"); + properties.put("account_id", "a1"); + properties.put("more", "less"); + FireboltProperties props = new FireboltProperties(properties); + assertEquals("db1", props.getDatabase()); + assertEquals("e1", props.getEngine()); + assertEquals("a1", props.getAccountId()); + assertEquals(Map.of("more", "less"), props.getAdditionalProperties()); + + props.addProperty("database", "db2"); + props.addProperty("engine", "e2"); + props.addProperty("account_id", "a1"); + assertThrows(IllegalStateException.class, () -> props.addProperty("account_id", "a2")); + props.addProperty("more", "is more"); + + assertEquals("db2", props.getDatabase()); + assertEquals("e2", props.getEngine()); + assertEquals("a1", props.getAccountId()); + assertEquals(Map.of("more", "is more"), props.getAdditionalProperties()); } @Test - void shouldSupportIntForBooleanProperties() { + void shouldUsePathParamAsDb() { Properties properties = new Properties(); - properties.put("path", "/example"); + properties.put("path", "example"); properties.put("host", "host"); - properties.put("ssl", "2"); - properties.put("compress", "0"); + assertEquals("example", new FireboltProperties(properties).getDatabase()); + } - assertTrue(FireboltProperties.of(properties).isSsl()); - assertFalse(FireboltProperties.of(properties).isCompress()); + @ParameterizedTest + @ValueSource(strings = {"$", "@"}) + void invalidDatabase(String db) { + Properties properties = new Properties(); + properties.put("path", db); + assertThrows(IllegalArgumentException.class, () -> new FireboltProperties(properties)); } @Test - void shouldUseCustomPortWhenProvided() { + void emptyCopy() { Properties properties = new Properties(); - properties.put("path", "/example"); + properties.put("path", "example"); properties.put("host", "host"); - properties.put("port", "999"); + assertEquals(new FireboltProperties(properties), FireboltProperties.copy(new FireboltProperties(properties))); + } - assertEquals(999, FireboltProperties.of(properties).getPort()); + @Test + void notEmptyCopy() { + assertEquals(new FireboltProperties(new Properties()), FireboltProperties.copy(new FireboltProperties(new Properties()))); } @Test - void shouldThrowExceptionWhenNoDbProvided() { + void shouldSupportBooleansForBooleanProperties() { Properties properties = new Properties(); + properties.put("path", "example"); properties.put("host", "host"); + properties.put("ssl", "true"); + properties.put("compress", "false"); - assertThrows(IllegalArgumentException.class, () -> FireboltProperties.of(properties)); + assertTrue(new FireboltProperties(properties).isSsl()); + assertFalse(new FireboltProperties(properties).isCompress()); } @Test - void shouldThrowExceptionWhenHostIsNotProvided() { + void shouldSupportIntForBooleanProperties() { Properties properties = new Properties(); - assertThrows(IllegalArgumentException.class, () -> FireboltProperties.of(properties)); + properties.put("path", "example"); + properties.put("host", "host"); + properties.put("ssl", "2"); + properties.put("compress", "0"); + + assertTrue(new FireboltProperties(properties).isSsl()); + assertFalse(new FireboltProperties(properties).isCompress()); } @Test - void shouldThrowExceptionWhenDbPathFormatIsInvalid() { + void shouldUseCustomPortWhenProvided() { Properties properties = new Properties(); - properties.put("path", ""); + properties.put("path", "example"); properties.put("host", "host"); + properties.put("port", "999"); - assertThrows(IllegalArgumentException.class, () -> FireboltProperties.of(properties)); + assertEquals(999, new FireboltProperties(properties).getPort()); } @Test - void shouldNotReturnAliasAsCustomProperty() { + void shouldUseSystemEngineWhenNoDbOrEngineProvided() { Properties properties = new Properties(); - properties.put("path", ""); - properties.put("host", "host"); - - assertThrows(IllegalArgumentException.class, () -> FireboltProperties.of(properties)); + FireboltProperties fireboltProperties = new FireboltProperties(properties); + assertTrue(fireboltProperties.isSystemEngine()); + assertNull(fireboltProperties.getEngine()); + assertNull(fireboltProperties.getDatabase()); + assertFalse(fireboltProperties.isCompress()); } @Test @@ -137,7 +178,52 @@ void shouldSupportUserClientsAndDrivers() { properties.put("host", "host"); properties.put("database", "db"); - assertEquals(clients, FireboltProperties.of(properties).getUserClients()); - assertEquals(drivers, FireboltProperties.of(properties).getUserDrivers()); + assertEquals(clients, new FireboltProperties(properties).getUserClients()); + assertEquals(drivers, new FireboltProperties(properties).getUserDrivers()); } + + @Test + void noEngineNoDbSystemEngine() { + assertNull(new FireboltProperties(new Properties()).getEngine()); + } + + @ParameterizedTest + @CsvSource(value = { + "env, qa,,api.qa.firebolt.io,qa", + "environment, test,,api.test.firebolt.io,test", + "env, staging,super-host.com,super-host.com,staging", + "env,,my-host.com,my-host.com,app", + "env,,api.dev.firebolt.io,api.dev.firebolt.io,dev", + "env,,something.io,something.io,app", // not standard host, no configured environment -> default environment + ",,,api.app.firebolt.io,app", // no host, no environment -> default environment (app) and default host api.app.firebolt.io + ",,api.app.firebolt.io,api.app.firebolt.io,app", // no configured environment, discover default environment from host + ",,api.dev.firebolt.io,api.dev.firebolt.io,dev", // no configured environment, discover not default environment from host + }, delimiter = ',') + void hostAndEnvironment(String envKey, String envValue, String host, String expectedHost, String expectedEnvironment) { + Properties properties = properties(envKey, envValue, host); + assertEquals(expectedHost, new FireboltProperties(properties).getHost()); + assertEquals(expectedEnvironment, new FireboltProperties(properties).getEnvironment()); + } + + @ParameterizedTest + @CsvSource(value = { + "env,app,api.dev.firebolt.io", + "env,qa,api.app.firebolt.io", + }, delimiter = ',') + void environmentDoesNotMatch(String envKey, String envValue, String host) { + Properties properties = properties(envKey, envValue, host); + assertThrows(IllegalStateException.class, () -> new FireboltProperties(properties)); + } + + private Properties properties(String envKey, String envValue, String host) { + Properties properties = new Properties(); + if (envValue != null) { + properties.put(envKey, envValue); + } + if (host != null) { + properties.put("host", host); + } + return properties; + } + } diff --git a/src/test/java/com/firebolt/jdbc/exception/ServerErrorTest.java b/src/test/java/com/firebolt/jdbc/exception/ServerErrorTest.java new file mode 100644 index 00000000..dc01ab01 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/exception/ServerErrorTest.java @@ -0,0 +1,34 @@ +package com.firebolt.jdbc.exception; + +import org.json.JSONObject; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static com.firebolt.jdbc.exception.ServerError.Error; +import static com.firebolt.jdbc.exception.ServerError.Error.Location; +import static com.firebolt.jdbc.exception.ServerError.Error.Severity; +import static com.firebolt.jdbc.exception.ServerError.Error.Source; +import static com.firebolt.jdbc.exception.ServerError.Query; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ServerErrorTest { + protected static Stream parse() { + return Stream.of( + Arguments.of("{}", new ServerError(null, null)), + Arguments.of("{\"query\": {}, \"errors\": []}", new ServerError(new Query(null, null, null), new Error[0])), + Arguments.of("{\"query\": {\"query_id\": \"qid\", \"request_id\": \"rid\", \"query_label\": \"ql\"}, \"errors\": []}", new ServerError(new Query("qid", "rid", "ql"), new Error[0])), + Arguments.of("{\"errors\": [{}]}", new ServerError(null, new Error[] {new Error(null, null, null, Source.UNKNOWN, null, null, null, null)})), + Arguments.of("{\"errors\": [{\"code\": \"c1\", \"name\": \"name1\", \"severity\": \"ERROR\", \"source\": \"System Error\", \"description\": \"description1\", \"resolution\": \"resolution1\", \"helpLink\": \"http://help1.com\", \"location\": {\"failingLine\": 1, \"startOffset\": 10, \"endOffset\": 100}}]}", + new ServerError(null, new Error[] {new Error("c1", "name1", Severity.ERROR, Source.SYSTEM_ERROR, "description1", "resolution1", "http://help1.com", new Location(1, 10, 100))})) + ); + } + + @ParameterizedTest + @MethodSource("parse") + void parse(String json, ServerError expected) { + assertEquals(expected, new ServerError(new JSONObject(json))); + } +} diff --git a/src/test/java/com/firebolt/jdbc/log/JDKLoggerTest.java b/src/test/java/com/firebolt/jdbc/log/JDKLoggerTest.java deleted file mode 100644 index a9ba09cc..00000000 --- a/src/test/java/com/firebolt/jdbc/log/JDKLoggerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firebolt.jdbc.log; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - -class JDKLoggerTest { - - @Test - void shouldLogWithCorrectLevel() { - try (MockedStatic mockedLogger = Mockito.mockStatic(Logger.class)) { - Logger logger = mock(Logger.class); - mockedLogger.when(() -> java.util.logging.Logger.getLogger(any())).thenReturn(logger); - JDKLogger jdkLogger = new JDKLogger("myTest"); - jdkLogger.debug("This is a debug log"); - jdkLogger.warn("This is a warning log"); - jdkLogger.error("This is an error log"); - jdkLogger.trace("This is a trace log"); - verify(logger).log(Level.FINE, "This is a debug log"); - verify(logger).log(Level.WARNING, "This is a warning log"); - verify(logger).log(Level.SEVERE, "This is an error log"); - verify(logger).log(Level.FINEST, "This is a trace log"); - } - } - - @Test - void shouldLogWithCorrectArgumentIndexes() { - try (MockedStatic mockedLogger = Mockito.mockStatic(Logger.class)) { - Logger logger = mock(Logger.class); - mockedLogger.when(() -> java.util.logging.Logger.getLogger(any())).thenReturn(logger); - JDKLogger jdkLogger = new JDKLogger("myTest"); - jdkLogger.debug("This debug log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - jdkLogger.warn("This warning log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - jdkLogger.error("This error log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - jdkLogger.trace("This trace log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - Object[] args = new Object[] { "arg1", "arg2", "arg3" }; - verify(logger).log(Level.FINE, "This debug log has some arguments: {0}, {1}, {2}", args); - verify(logger).log(Level.WARNING, "This warning log has some arguments: {0}, {1}, {2}", args); - verify(logger).log(Level.SEVERE, "This error log has some arguments: {0}, {1}, {2}", args); - verify(logger).log(Level.FINEST, "This trace log has some arguments: {0}, {1}, {2}", args); - } - } - - @Test - void shouldLogWithThrowable() { - try (MockedStatic mockedLogger = Mockito.mockStatic(Logger.class)) { - Logger logger = mock(Logger.class); - mockedLogger.when(() -> java.util.logging.Logger.getLogger(any())).thenReturn(logger); - JDKLogger jdkLogger = new JDKLogger("myTest"); - IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); - jdkLogger.debug("This debug log has an exception", illegalArgumentException); - jdkLogger.warn("This warning log has an exception", illegalArgumentException); - jdkLogger.error("This error log has an exception", illegalArgumentException); - jdkLogger.trace("This trace log has an exception", illegalArgumentException); - verify(logger).log(Level.FINE, "This debug log has an exception", illegalArgumentException); - verify(logger).log(Level.WARNING, "This warning log has an exception", illegalArgumentException); - verify(logger).log(Level.SEVERE, "This error log has an exception", illegalArgumentException); - verify(logger).log(Level.FINEST, "This trace log has an exception", illegalArgumentException); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/log/LogLevelExample.java b/src/test/java/com/firebolt/jdbc/log/LogLevelExample.java deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/com/firebolt/jdbc/log/SLF4JLoggerTest.java b/src/test/java/com/firebolt/jdbc/log/SLF4JLoggerTest.java deleted file mode 100644 index 6b51d0be..00000000 --- a/src/test/java/com/firebolt/jdbc/log/SLF4JLoggerTest.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.firebolt.jdbc.log; - -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -class SLF4JLoggerTest { - - @Test - void shouldLogWithCorrectLevel() { - try (MockedStatic mockedLoggerFactory = Mockito.mockStatic(LoggerFactory.class)) { - Logger logger = mock(Logger.class); - mockedLoggerFactory.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger); - SLF4JLogger slf4JLogger = new SLF4JLogger("logger"); - slf4JLogger.debug("This is a debug log"); - slf4JLogger.warn("This is a warning log"); - slf4JLogger.error("This is an error log"); - slf4JLogger.trace("This is a trace log"); - verify(logger).debug("This is a debug log"); - verify(logger).warn("This is a warning log"); - verify(logger).error("This is an error log"); - verify(logger).trace("This is a trace log"); - } - } - - @Test - void shouldLogWithArguments() { - try (MockedStatic mockedLoggerFactory = Mockito.mockStatic(LoggerFactory.class)) { - Logger logger = mock(Logger.class); - mockedLoggerFactory.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger); - SLF4JLogger slf4JLogger = new SLF4JLogger("logger"); - slf4JLogger.debug("This debug log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - slf4JLogger.warn("This warning log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - slf4JLogger.error("This error log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - slf4JLogger.trace("This trace log has some arguments: {}, {}, {}", "arg1", "arg2", "arg3"); - Object[] args = new Object[] { "arg1", "arg2", "arg3" }; - verify(logger).debug("This debug log has some arguments: {}, {}, {}", args); - verify(logger).warn("This warning log has some arguments: {}, {}, {}", args); - verify(logger).error("This error log has some arguments: {}, {}, {}", args); - verify(logger).trace("This trace log has some arguments: {}, {}, {}", args); - } - } - - @Test - void shouldLogWithThrowable() { - try (MockedStatic mockedLoggerFactory = Mockito.mockStatic(LoggerFactory.class)) { - Logger logger = mock(Logger.class); - mockedLoggerFactory.when(() -> LoggerFactory.getLogger(anyString())).thenReturn(logger); - SLF4JLogger slf4JLogger = new SLF4JLogger("logger"); - IllegalArgumentException illegalArgumentException = new IllegalArgumentException(); - slf4JLogger.debug("This debug log has an exception", illegalArgumentException); - slf4JLogger.warn("This warning log has an exception", illegalArgumentException); - slf4JLogger.error("This error log has an exception", illegalArgumentException); - slf4JLogger.trace("This trace log has an exception", illegalArgumentException); - verify(logger).debug("This debug log has an exception", illegalArgumentException); - verify(logger).warn("This warning log has an exception", illegalArgumentException); - verify(logger).error("This error log has an exception", illegalArgumentException); - verify(logger).trace("This trace log has an exception", illegalArgumentException); - } - } - -} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadataTest.java b/src/test/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadataTest.java index d798c6b2..806a25c7 100644 --- a/src/test/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadataTest.java +++ b/src/test/java/com/firebolt/jdbc/metadata/FireboltDatabaseMetadataTest.java @@ -8,7 +8,6 @@ import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.testutils.AssertionUtil; -import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,14 +27,16 @@ import java.sql.DatabaseMetaData; import java.sql.Driver; import java.sql.ResultSet; +import java.sql.RowIdLifetime; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.sql.Wrapper; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; import static com.firebolt.jdbc.metadata.MetadataColumns.BUFFER_LENGTH; @@ -45,12 +46,16 @@ import static com.firebolt.jdbc.metadata.MetadataColumns.COLUMN_SIZE; import static com.firebolt.jdbc.metadata.MetadataColumns.DATA_TYPE; import static com.firebolt.jdbc.metadata.MetadataColumns.DECIMAL_DIGITS; +import static com.firebolt.jdbc.metadata.MetadataColumns.GRANTEE; +import static com.firebolt.jdbc.metadata.MetadataColumns.GRANTOR; import static com.firebolt.jdbc.metadata.MetadataColumns.IS_AUTOINCREMENT; import static com.firebolt.jdbc.metadata.MetadataColumns.IS_GENERATEDCOLUMN; +import static com.firebolt.jdbc.metadata.MetadataColumns.IS_GRANTABLE; import static com.firebolt.jdbc.metadata.MetadataColumns.IS_NULLABLE; import static com.firebolt.jdbc.metadata.MetadataColumns.NULLABLE; import static com.firebolt.jdbc.metadata.MetadataColumns.NUM_PREC_RADIX; import static com.firebolt.jdbc.metadata.MetadataColumns.ORDINAL_POSITION; +import static com.firebolt.jdbc.metadata.MetadataColumns.PRIVILEGE; import static com.firebolt.jdbc.metadata.MetadataColumns.REF_GENERATION; import static com.firebolt.jdbc.metadata.MetadataColumns.REMARKS; import static com.firebolt.jdbc.metadata.MetadataColumns.SCOPE_CATALOG; @@ -70,13 +75,17 @@ import static com.firebolt.jdbc.metadata.MetadataColumns.TYPE_SCHEM; import static com.firebolt.jdbc.type.FireboltDataType.INTEGER; import static com.firebolt.jdbc.type.FireboltDataType.TEXT; +import static java.sql.Connection.TRANSACTION_READ_COMMITTED; +import static java.sql.Connection.TRANSACTION_READ_UNCOMMITTED; +import static java.sql.Connection.TRANSACTION_REPEATABLE_READ; +import static java.sql.Connection.TRANSACTION_SERIALIZABLE; +import static java.sql.DatabaseMetaData.sqlStateSQL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; @@ -85,24 +94,21 @@ @ExtendWith(MockitoExtension.class) class FireboltDatabaseMetadataTest { - @Mock private FireboltConnection fireboltConnection; @Mock private FireboltStatement statement; -// @InjectMocks - private FireboltDatabaseMetadata fireboltDatabaseMetadata; + private DatabaseMetaData fireboltDatabaseMetadata; @BeforeEach void init() throws SQLException { fireboltDatabaseMetadata = new FireboltDatabaseMetadata("jdbc:firebolt:host", fireboltConnection); - lenient().when(fireboltConnection.createStatement(any())).thenReturn(statement); lenient().when(fireboltConnection.createStatement()).thenReturn(statement); lenient().when(fireboltConnection.getCatalog()).thenReturn("db_name"); - lenient().when(fireboltConnection.getSessionProperties()).thenReturn(FireboltProperties.builder().database("my-db").build()); - lenient().when(statement.executeQuery(anyString())).thenReturn(FireboltResultSet.empty()); + lenient().when(fireboltConnection.getSessionProperties()).thenReturn(FireboltProperties.builder().database("my-db").principal("the-user").build()); + lenient().when(statement.executeQuery(anyString())).thenReturn(createResultSet(new ByteArrayInputStream(new byte[0]))); } @Test @@ -133,20 +139,15 @@ void shouldReturnSchemas() throws SQLException { .columns(List.of( Column.builder().name(TABLE_SCHEM).type(TEXT).build(), Column.builder().name(TABLE_CATALOG).type(TEXT).build())) - .rows(List.of(List.of("public", "my-db"), List.of("information_schema", "my-db"), List.of("catalog", "my-db"))) + .rows(List.of(List.of("public", "my-db"), List.of("information_schema", "my-db"))) .build()); + when(statement.executeQuery(anyString())).thenReturn(createResultSet(getInputStreamForGetSchemas())); ResultSet actualResultSet = fireboltDatabaseMetadata.getSchemas(); AssertionUtil.assertResultSetEquality(expectedResultSet, actualResultSet); } - @Test - void shouldNotReturnAnyProcedureColumns() throws SQLException { - AssertionUtil.assertResultSetEquality(FireboltResultSet.empty(), - fireboltDatabaseMetadata.getProcedureColumns(null, null, null, null)); - } - @Test void shouldReturnDatabaseProduct() throws SQLException { assertEquals("Firebolt", fireboltDatabaseMetadata.getDatabaseProductName()); @@ -188,7 +189,7 @@ void shouldReturnFalseWhenIncorrectTransactionIsolationLevelIsSpecified() throws // ResultSet expectedResultSet = FireboltDatabaseMetadataResult.builder() // .columns(Arrays.asList(Column.builder().name(TABLE_SCHEM).type(STRING).build(), // Column.builder().name(TABLE_CATALOG).type(STRING).build())) -// .rows(Arrays.asList(Arrays.asList("Tutorial_11_04", "default"), Arrays.asList("system", "default"))) +// .rows(Arrays.asList(Arrays.asList("Tutorial_11_04", "default"), Arrays.asList(SYSTEM_ENGINE_NAME, "default"))) // .build().toResultSet(); // // verifyResultSetEquality(expectedResultSet, resultSet); @@ -246,41 +247,59 @@ void shouldGetColumns() throws SQLException { "NO"))) .build()); - when(statement.executeQuery(expectedQuery)) - .thenReturn(new FireboltResultSet(this.getInputStreamForGetColumns())); + when(statement.executeQuery(expectedQuery)).thenReturn(createResultSet(getInputStreamForGetColumns())); ResultSet resultSet = fireboltDatabaseMetadata.getColumns("a", "b", "c", "d"); verify(statement).executeQuery(expectedQuery); AssertionUtil.assertResultSetEquality(expectedResultSet, resultSet); } + @Test + void shouldGetColumnPrivileges() throws SQLException { + String expectedQuery = "SELECT table_schema, table_name, column_name, data_type, column_default, is_nullable, ordinal_position FROM information_schema.columns WHERE table_name LIKE 'c' AND column_name LIKE 'd' AND table_schema LIKE 'b'"; + + ResultSet expectedResultSet = FireboltResultSet.of(QueryResult.builder() + .columns(Arrays.asList(Column.builder().name(TABLE_CAT).type(TEXT).build(), + Column.builder().name(TABLE_SCHEM).type(TEXT).build(), + Column.builder().name(TABLE_NAME).type(TEXT).build(), + Column.builder().name(COLUMN_NAME).type(TEXT).build(), + Column.builder().name(GRANTOR).type(TEXT).build(), + Column.builder().name(GRANTEE).type(TEXT).build(), + Column.builder().name(PRIVILEGE).type(TEXT).build(), + Column.builder().name(IS_GRANTABLE).type(TEXT).build())) + .rows(Collections.singletonList(Arrays.asList("db_name", "Tutorial_11_04", // schema + "D2_TIMESTAMP", // table name + "id", // column name + null, // grantor + null, // grantee + null, // privilege + "NO"))) + .build()); + + when(statement.executeQuery(expectedQuery)).thenReturn(createResultSet(getInputStreamForGetColumns())); + + ResultSet resultSet = fireboltDatabaseMetadata.getColumnPrivileges("a", "b", "c", "d"); + verify(statement).executeQuery(expectedQuery); + AssertionUtil.assertResultSetEquality(expectedResultSet, resultSet); + } + @Test void shouldGetTypeInfo() throws SQLException { ResultSet resultSet = fireboltDatabaseMetadata.getTypeInfo(); - ResultSet expectedTypeInfo = new FireboltResultSet(this.getExpectedTypeInfo()); + ResultSet expectedTypeInfo = createResultSet(getExpectedTypeInfo()); AssertionUtil.assertResultSetEquality(expectedTypeInfo, resultSet); } @Test void shouldGetTables() throws SQLException { - String expectedSqlForTables = "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_schema LIKE 'def%' AND table_name LIKE 'tab%' AND table_type NOT LIKE 'EXTERNAL' order by table_schema, table_name"; - - String expectedSqlForViews = "SELECT table_schema, table_name FROM information_schema.views WHERE table_schema LIKE 'def%' AND table_name LIKE 'tab%' order by table_schema, table_name"; - - when(statement.executeQuery(expectedSqlForTables)) - .thenReturn(new FireboltResultSet(getInputStreamForGetTables())); - when(statement.executeQuery(expectedSqlForViews)).thenReturn(FireboltResultSet.empty()); - + String expectedSql = "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type IN ('BASE TABLE', 'DIMENSION', 'FACT', 'VIEW') AND table_schema LIKE 'def%' AND table_name LIKE 'tab%' order by table_schema, table_name"; + when(statement.executeQuery(expectedSql)).thenReturn(createResultSet(getInputStreamForGetTables())); ResultSet resultSet = fireboltDatabaseMetadata.getTables("catalog", "def%", "tab%", null); + verify(statement).executeQuery(expectedSql); - verify(statement).executeQuery(expectedSqlForTables); - verify(statement).executeQuery(expectedSqlForViews); - - List> expectedRows = new ArrayList<>(); - expectedRows.add(Arrays.asList("db_name", "public", "ex_lineitem", "TABLE", null, null, null, null, null, null, - null, null, null)); - expectedRows.add(Arrays.asList("db_name", "public", "test_1", "TABLE", null, null, null, null, null, null, null, - null, null)); + List> expectedRows = List.of( + Arrays.asList("db_name", "public", "ex_lineitem", "TABLE", null, null, null, null, null, null, null, null, null), + Arrays.asList("db_name", "public", "test_1", "TABLE", null, null, null, null, null, null, null, null, null)); ResultSet expectedResultSet = FireboltResultSet.of(QueryResult.builder() .columns(Arrays.asList(Column.builder().name(TABLE_CAT).type(TEXT).build(), @@ -298,19 +317,43 @@ void shouldGetTables() throws SQLException { AssertionUtil.assertResultSetEquality(expectedResultSet, resultSet); } + @Test + void shouldGetTablePrivileges() throws SQLException { + String expectedSql = "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type IN ('BASE TABLE', 'DIMENSION', 'FACT') AND table_schema LIKE 'def%' AND table_name LIKE 'tab%' order by table_schema, table_name"; + when(statement.executeQuery(expectedSql)).thenReturn(createResultSet(getInputStreamForGetTables())); + ResultSet resultSet = fireboltDatabaseMetadata.getTablePrivileges("catalog", "def%", "tab%"); + verify(statement).executeQuery(expectedSql); + + List> expectedRows = List.of( + Arrays.asList("db_name", "public", "ex_lineitem", null, null, null, "NO"), + Arrays.asList("db_name", "public", "test_1", null, null, null, "NO")); + + ResultSet expectedResultSet = FireboltResultSet.of(QueryResult.builder() + .columns(Arrays.asList(Column.builder().name(TABLE_CAT).type(TEXT).build(), + Column.builder().name(TABLE_SCHEM).type(TEXT).build(), + Column.builder().name(TABLE_NAME).type(TEXT).build(), + Column.builder().name(GRANTOR).type(TEXT).build(), + Column.builder().name(GRANTEE).type(TEXT).build(), + Column.builder().name(PRIVILEGE).type(TEXT).build(), + Column.builder().name(IS_GRANTABLE).type(TEXT).build())) + .rows(expectedRows).build()); + + AssertionUtil.assertResultSetEquality(expectedResultSet, resultSet); + } + @Test void shouldGetDriverMajorVersion() { - assertEquals(2, fireboltDatabaseMetadata.getDriverMajorVersion()); + assertEquals(3, fireboltDatabaseMetadata.getDriverMajorVersion()); } @Test void shouldGetDriverMinorVersion() { - assertEquals(4, fireboltDatabaseMetadata.getDriverMinorVersion()); + assertEquals(0, fireboltDatabaseMetadata.getDriverMinorVersion()); } @Test void shouldGetDriverVersion() throws SQLException { - assertEquals("2.4.6", fireboltDatabaseMetadata.getDriverVersion()); + assertEquals("3.0.4-SNAPSHOT", fireboltDatabaseMetadata.getDriverVersion()); } @Test @@ -325,32 +368,26 @@ void shouldGetJdbcManorVersion() throws SQLException { @Test void shouldGetDatabaseProductVersion() throws SQLException { - Statement statement = mock(FireboltStatement.class); - when(fireboltConnection.createStatement()).thenReturn(statement); - when(fireboltConnection.getEngine()).thenReturn("test"); - when(statement.executeQuery("SELECT version FROM information_schema.engines WHERE engine_name iLIKE 'test%'")) - .thenReturn(new FireboltResultSet(getInputStreamForGetVersion())); + mockGetDatabaseVersion(); assertEquals("abcd_xxx_123", fireboltDatabaseMetadata.getDatabaseProductVersion()); } @Test void shouldGetDatabaseMajorVersion() throws SQLException { - Statement statement = mock(FireboltStatement.class); - when(fireboltConnection.createStatement()).thenReturn(statement); - when(fireboltConnection.getEngine()).thenReturn("test"); - when(statement.executeQuery("SELECT version FROM information_schema.engines WHERE engine_name iLIKE 'test%'")) - .thenReturn(new FireboltResultSet(getInputStreamForGetVersion())); + mockGetDatabaseVersion(); assertEquals(0, fireboltDatabaseMetadata.getDatabaseMajorVersion()); } @Test void shouldGetDatabaseMinorVersion() throws SQLException { + mockGetDatabaseVersion(); + assertEquals(0, fireboltDatabaseMetadata.getDatabaseMinorVersion()); + } + + private void mockGetDatabaseVersion() throws SQLException { Statement statement = mock(FireboltStatement.class); when(fireboltConnection.createStatement()).thenReturn(statement); - when(fireboltConnection.getEngine()).thenReturn("test"); - when(statement.executeQuery("SELECT version FROM information_schema.engines WHERE engine_name iLIKE 'test%'")) - .thenReturn(new FireboltResultSet(getInputStreamForGetVersion())); - assertEquals(0, fireboltDatabaseMetadata.getDatabaseMinorVersion()); + when(statement.executeQuery("SELECT VERSION()")).thenReturn(createResultSet(getInputStreamForGetVersion())); } @Test @@ -358,24 +395,347 @@ void isReadOnly() throws SQLException { assertFalse(fireboltDatabaseMetadata.isReadOnly()); } + @Test + void nullSorting() throws SQLException { + assertFalse(fireboltDatabaseMetadata.nullsAreSortedHigh()); + assertTrue(fireboltDatabaseMetadata.nullsAreSortedLow()); + assertFalse(fireboltDatabaseMetadata.nullsAreSortedAtStart()); + assertTrue(fireboltDatabaseMetadata.nullsAreSortedAtEnd()); + } + + @Test + void useLocalFiles() throws SQLException { + assertFalse(fireboltDatabaseMetadata.usesLocalFiles()); + assertFalse(fireboltDatabaseMetadata.usesLocalFilePerTable()); + } + + @Test + void identifiersCase() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsMixedCaseIdentifiers()); + assertFalse(fireboltDatabaseMetadata.storesMixedCaseIdentifiers()); + assertFalse(fireboltDatabaseMetadata.storesUpperCaseIdentifiers()); + assertTrue(fireboltDatabaseMetadata.storesLowerCaseIdentifiers()); + } + + @Test + void quotedIdentifiersCase() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsMixedCaseQuotedIdentifiers()); + assertTrue(fireboltDatabaseMetadata.storesMixedCaseQuotedIdentifiers()); + assertFalse(fireboltDatabaseMetadata.storesUpperCaseQuotedIdentifiers()); + assertFalse(fireboltDatabaseMetadata.storesLowerCaseQuotedIdentifiers()); + } + + @Test + void getIdentifierQuoteString() throws SQLException { + assertEquals("\"", fireboltDatabaseMetadata.getIdentifierQuoteString()); + } + + @Test + void getSearchStringEscape() throws SQLException { + assertEquals("\\", fireboltDatabaseMetadata.getSearchStringEscape()); + } + + @Test + void getExtraNameCharacters() throws SQLException { + assertEquals("", fireboltDatabaseMetadata.getExtraNameCharacters()); + } + + @Test + void supportsAlterTable() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsAlterTableWithAddColumn()); + assertFalse(fireboltDatabaseMetadata.supportsAlterTableWithDropColumn()); + } + + @Test + void supportsColumnAliasing() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsColumnAliasing()); + } + + @Test + void nullPlusNonNullIsNull() throws SQLException { + assertTrue(fireboltDatabaseMetadata.nullPlusNonNullIsNull()); + } + + @Test + void supportsConvert() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsConvert()); + assertFalse(fireboltDatabaseMetadata.supportsConvert(Types.INTEGER, Types.VARCHAR)); + } + + + @Test + void correlationNames() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsTableCorrelationNames()); + assertFalse(fireboltDatabaseMetadata.supportsDifferentTableCorrelationNames()); + } + + @Test + void supportsExpressionsInOrderBy() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsExpressionsInOrderBy()); + assertTrue(fireboltDatabaseMetadata.supportsOrderByUnrelated()); + } + + @Test + void supportsGroupBy() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsGroupBy()); + } + + @Test + void supportsGroupByUnrelated() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsGroupByUnrelated()); + } + + @Test + void supportsGroupByBeyondSelect() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsGroupByBeyondSelect()); + } + + @Test + void supportsLikeEscapeClause() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsLikeEscapeClause()); + } + + @Test + void supportsMultipleResultSets() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsMultipleResultSets()); + } + + @Test + void supportsMinimumSQLGrammar() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsMinimumSQLGrammar()); + } + + @Test + void supportsCoreSQLGrammar() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsCoreSQLGrammar()); + } + + @Test + void supportsExtendedSQLGrammar() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsExtendedSQLGrammar()); + } + + @Test + void supportsANSI92EntryLevelSQL() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsANSI92EntryLevelSQL()); + } + + @Test + void supportsANSI92IntermediateSQL() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsANSI92IntermediateSQL()); + } + + @Test + void supportsANSI92FullSQL() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsANSI92FullSQL()); + } + + @Test + void supportsIntegrityEnhancementFacility() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsIntegrityEnhancementFacility()); + } + + @Test + void supportsTransactions() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsTransactions()); + assertFalse(fireboltDatabaseMetadata.supportsMultipleTransactions()); + assertTrue(fireboltDatabaseMetadata.supportsTransactionIsolationLevel(Connection.TRANSACTION_NONE)); + assertEquals(Connection.TRANSACTION_NONE, fireboltDatabaseMetadata.getDefaultTransactionIsolation()); + assertFalse(fireboltDatabaseMetadata.supportsDataManipulationTransactionsOnly()); + assertFalse(fireboltDatabaseMetadata.supportsDataDefinitionAndDataManipulationTransactions()); + assertFalse(fireboltDatabaseMetadata.dataDefinitionCausesTransactionCommit()); + + for (int level : new int[] {TRANSACTION_READ_UNCOMMITTED, TRANSACTION_READ_COMMITTED, TRANSACTION_REPEATABLE_READ, TRANSACTION_SERIALIZABLE}) { + assertFalse(fireboltDatabaseMetadata.supportsTransactionIsolationLevel(level)); + } + assertFalse(fireboltDatabaseMetadata.supportsSavepoints()); + assertFalse(fireboltDatabaseMetadata.autoCommitFailureClosesAllResultSets()); + + assertFalse(fireboltDatabaseMetadata.supportsOpenCursorsAcrossCommit()); + assertFalse(fireboltDatabaseMetadata.supportsOpenCursorsAcrossRollback()); + assertFalse(fireboltDatabaseMetadata.supportsOpenStatementsAcrossCommit()); + assertFalse(fireboltDatabaseMetadata.supportsOpenStatementsAcrossRollback()); + assertFalse(fireboltDatabaseMetadata.dataDefinitionIgnoredInTransactions()); + } + + @Test + void supportsBatchUpdates() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsBatchUpdates()); + } + + @Test + void supportsResultSetType() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY)); + assertFalse(fireboltDatabaseMetadata.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE)); + assertFalse(fireboltDatabaseMetadata.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE)); + } + + @Test + void supportJoins() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsOuterJoins()); + assertTrue(fireboltDatabaseMetadata.supportsFullOuterJoins()); + assertTrue(fireboltDatabaseMetadata.supportsLimitedOuterJoins()); + } + + @Test + void supportTerms() throws SQLException { + assertEquals("schema", fireboltDatabaseMetadata.getSchemaTerm()); + assertEquals("procedure", fireboltDatabaseMetadata.getProcedureTerm()); + assertEquals("database", fireboltDatabaseMetadata.getCatalogTerm()); + } + + @Test + void supportsCatalogs() throws SQLException { + assertFalse(fireboltDatabaseMetadata.isCatalogAtStart()); + assertEquals(".", fireboltDatabaseMetadata.getCatalogSeparator()); + assertFalse(fireboltDatabaseMetadata.supportsCatalogsInDataManipulation()); + assertFalse(fireboltDatabaseMetadata.supportsCatalogsInProcedureCalls()); + assertFalse(fireboltDatabaseMetadata.supportsCatalogsInTableDefinitions()); + assertFalse(fireboltDatabaseMetadata.supportsCatalogsInIndexDefinitions()); + assertFalse(fireboltDatabaseMetadata.supportsCatalogsInPrivilegeDefinitions()); + } + + @Test + void supportsSchemas() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsSchemasInDataManipulation()); + assertFalse(fireboltDatabaseMetadata.supportsSchemasInProcedureCalls()); + assertFalse(fireboltDatabaseMetadata.supportsSchemasInTableDefinitions()); + assertFalse(fireboltDatabaseMetadata.supportsSchemasInIndexDefinitions()); + assertFalse(fireboltDatabaseMetadata.supportsSchemasInPrivilegeDefinitions()); + } + + @Test + void supportsUnions() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsUnion()); + assertTrue(fireboltDatabaseMetadata.supportsUnionAll()); + } + + @Test + void checkLimits() throws SQLException { + assertEquals(63, fireboltDatabaseMetadata.getMaxColumnNameLength()); + assertEquals(63, fireboltDatabaseMetadata.getMaxSchemaNameLength()); + assertEquals(63, fireboltDatabaseMetadata.getMaxCatalogNameLength()); + assertEquals(63, fireboltDatabaseMetadata.getMaxTableNameLength()); + assertEquals(1000, fireboltDatabaseMetadata.getMaxColumnsInTable()); + assertEquals(0x40000, fireboltDatabaseMetadata.getMaxBinaryLiteralLength()); + assertEquals(0x40000, fireboltDatabaseMetadata.getMaxCharLiteralLength()); + assertEquals(65536, fireboltDatabaseMetadata.getMaxColumnsInGroupBy()); + assertEquals(16384, fireboltDatabaseMetadata.getMaxColumnsInOrderBy()); + assertEquals(8192, fireboltDatabaseMetadata.getMaxColumnsInSelect()); + assertEquals(0, fireboltDatabaseMetadata.getMaxColumnsInIndex()); + assertEquals(0, fireboltDatabaseMetadata.getMaxConnections()); + assertEquals(0, fireboltDatabaseMetadata.getMaxCursorNameLength()); + assertEquals(0, fireboltDatabaseMetadata.getMaxIndexLength()); + assertEquals(0, fireboltDatabaseMetadata.getMaxProcedureNameLength()); + assertEquals(0, fireboltDatabaseMetadata.getMaxStatements()); + assertEquals(63, fireboltDatabaseMetadata.getMaxUserNameLength()); + assertEquals(0, fireboltDatabaseMetadata.getMaxTablesInSelect()); + assertEquals(0, fireboltDatabaseMetadata.getMaxRowSize()); + assertEquals(0, fireboltDatabaseMetadata.getMaxStatementLength()); + assertTrue(fireboltDatabaseMetadata.doesMaxRowSizeIncludeBlobs()); + } + + @Test + void supportsMultipleOpenResults() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsMultipleOpenResults()); + } + + @Test + void supportsGetGeneratedKeys() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsGetGeneratedKeys()); + } + + @Test + void supportsNonNullableColumns() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsNonNullableColumns()); + } + + @Test + void supportsNamedParameters() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsNamedParameters()); + } + + @Test + void holdability() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT)); + assertFalse(fireboltDatabaseMetadata.supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT)); + assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, fireboltDatabaseMetadata.getResultSetHoldability()); + } + + @Test + void getSQLStateType() throws SQLException { + assertEquals(sqlStateSQL, fireboltDatabaseMetadata.getSQLStateType()); + } + + @Test + void locatorsUpdateCopy() throws SQLException { + assertFalse(fireboltDatabaseMetadata.locatorsUpdateCopy()); + } + + @Test + void supportsStatementPooling() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsStatementPooling()); + } + + @Test + void getRowIdLifetime() throws SQLException { + assertEquals(RowIdLifetime.ROWID_UNSUPPORTED, fireboltDatabaseMetadata.getRowIdLifetime()); + } + + @Test + void supportsStoredFunctionsUsingCallSyntax() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsStoredFunctionsUsingCallSyntax()); + } + + @Test + void positioned() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsPositionedDelete()); + assertFalse(fireboltDatabaseMetadata.supportsPositionedUpdate()); + } + + @Test + void emptyResultSets() throws SQLException { + assertFalse(fireboltDatabaseMetadata.getProcedureColumns(null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getUDTs(null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getUDTs(null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getSuperTypes(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getSuperTables(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getAttributes(null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getProcedures(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getBestRowIdentifier(null, null, null, 0, false).next()); + assertFalse(fireboltDatabaseMetadata.getVersionColumns(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getPrimaryKeys(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getImportedKeys(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getExportedKeys(null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getCrossReference(null, null, null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getClientInfoProperties().next()); + assertFalse(fireboltDatabaseMetadata.getPseudoColumns(null, null, null, null).next()); + assertFalse(fireboltDatabaseMetadata.getIndexInfo(null, null, null, true, true).next()); + } + @Test void getStringFunctions() throws SQLException { - getFunctions(DatabaseMetaData::getStringFunctions); + getFunctions(DatabaseMetaData::getStringFunctions, "CONCAT", "SPLIT"); + } + + @Test + void getSQLKeywords() throws SQLException { + getFunctions(DatabaseMetaData::getSQLKeywords, "ACCOUNT", "COPY", "ENGINE", "TABLE"); } @Test void getNumericFunctions() throws SQLException { - getFunctions(DatabaseMetaData::getNumericFunctions); + getFunctions(DatabaseMetaData::getNumericFunctions, "ABS", "RANDOM"); } @Test void getSystemFunctions() throws SQLException { - getFunctions(DatabaseMetaData::getSystemFunctions); + getFunctions(DatabaseMetaData::getSystemFunctions, "VERSION"); } @Test void getTimeDateFunctions() throws SQLException { - getFunctions(DatabaseMetaData::getTimeDateFunctions); + getFunctions(DatabaseMetaData::getTimeDateFunctions, "DATE_ADD", "DATE_DIFF"); } @ParameterizedTest @@ -414,49 +774,120 @@ void isNotWrapperFor(Class clazz) throws SQLException { assertFalse(fireboltDatabaseMetadata.isWrapperFor(clazz)); } - void getFunctions(CheckedFunction getter, String functionNamePattern, boolean filled, boolean allowDuplicates) throws SQLException { + @Test + void generatedKeyAlwaysReturned() throws SQLException { + assertFalse(fireboltDatabaseMetadata.generatedKeyAlwaysReturned()); + } + + @Test + void allProceduresAreCallable() throws SQLException { + assertFalse(fireboltDatabaseMetadata.allProceduresAreCallable()); + } + + @Test + void allTablesAreSelectable() throws SQLException { + assertTrue(fireboltDatabaseMetadata.allTablesAreSelectable()); + } + + @Test + void getUserName() throws SQLException { + assertEquals("the-user", fireboltDatabaseMetadata.getUserName()); + } + + @Test + void supportsSelectForUpdate() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsSelectForUpdate()); + } + + @Test + void supportsStoredProcedures() throws SQLException { + assertFalse(fireboltDatabaseMetadata.supportsStoredProcedures()); + } + + @Test + void supportsSubqueries() throws SQLException { + assertTrue(fireboltDatabaseMetadata.supportsSubqueriesInComparisons()); + assertTrue(fireboltDatabaseMetadata.supportsSubqueriesInExists()); + assertTrue(fireboltDatabaseMetadata.supportsSubqueriesInIns()); + assertFalse(fireboltDatabaseMetadata.supportsSubqueriesInQuantifieds()); + assertTrue(fireboltDatabaseMetadata.supportsCorrelatedSubqueries()); + } + + + @ParameterizedTest + @CsvSource(value = { + ResultSet.TYPE_FORWARD_ONLY + "," + ResultSet.CONCUR_READ_ONLY + ",true", + ResultSet.TYPE_FORWARD_ONLY + "," + ResultSet.CONCUR_UPDATABLE + ",false", + ResultSet.TYPE_SCROLL_INSENSITIVE + "," + ResultSet.CONCUR_READ_ONLY + ",false", + ResultSet.TYPE_SCROLL_INSENSITIVE + "," + ResultSet.CONCUR_UPDATABLE + ",false", + ResultSet.TYPE_SCROLL_SENSITIVE + "," + ResultSet.CONCUR_READ_ONLY + ",false", + ResultSet.TYPE_SCROLL_SENSITIVE + "," + ResultSet.CONCUR_UPDATABLE + ",false", + }) + void supportsResultSetConcurrency(int type, int concurrency, boolean expected) throws SQLException { + assertEquals(expected, fireboltDatabaseMetadata.supportsResultSetConcurrency(type, concurrency)); + } + + @ParameterizedTest + @ValueSource(ints = {ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.TYPE_SCROLL_SENSITIVE}) + void writeability(int type) throws SQLException { + assertFalse(fireboltDatabaseMetadata.ownUpdatesAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.ownDeletesAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.ownInsertsAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.othersUpdatesAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.othersDeletesAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.othersInsertsAreVisible(type)); + assertFalse(fireboltDatabaseMetadata.updatesAreDetected(type)); + assertFalse(fireboltDatabaseMetadata.deletesAreDetected(type)); + assertFalse(fireboltDatabaseMetadata.insertsAreDetected(type)); + } + + private void getFunctions(CheckedFunction getter, String functionNamePattern, boolean filled, boolean allowDuplicates) throws SQLException { String previousFunction = null; int count = 0; - for (ResultSet rs = getter.apply(fireboltDatabaseMetadata); rs.next();) { - count++; - String functionName = rs.getString("FUNCTION_NAME"); - String specificName = rs.getString("SPECIFIC_NAME"); - assertNotNull(functionName); - assertNotNull(specificName); - assertEquals(functionName, specificName); - if (functionNamePattern != null) { - assertTrue(StringUtils.containsIgnoreCase(functionName, functionNamePattern)); - } - if (previousFunction != null) { - int functionNameComparison = previousFunction.compareToIgnoreCase(functionName); - if (allowDuplicates) { - assertTrue(functionNameComparison <= 0); - } else { - assertTrue(functionNameComparison < 0); + Pattern pattern = functionNamePattern == null ? null : Pattern.compile(functionNamePattern, Pattern.CASE_INSENSITIVE); + try (ResultSet rs = getter.apply(fireboltDatabaseMetadata)) { + while (rs.next()) { + count++; + String functionName = rs.getString("FUNCTION_NAME"); + String specificName = rs.getString("SPECIFIC_NAME"); + assertNotNull(functionName); + assertNotNull(specificName); + assertEquals(functionName, specificName); + if (pattern != null) { + assertTrue(pattern.matcher(functionName).find()); + } + if (previousFunction != null) { + int functionNameComparison = previousFunction.compareToIgnoreCase(functionName); + if (allowDuplicates) { + assertTrue(functionNameComparison <= 0); + } else { + assertTrue(functionNameComparison < 0); + } } + previousFunction = functionName; } - previousFunction = functionName; } assertEquals(filled, count > 0); } private InputStream getInputStreamForGetColumns() { - return FireboltDatabaseMetadata.class - .getResourceAsStream("/responses/metadata/firebolt-response-get-columns-example"); + return FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/firebolt-response-get-columns-example"); } private InputStream getInputStreamForGetTables() { - return FireboltDatabaseMetadata.class - .getResourceAsStream("/responses/metadata/firebolt-response-get-tables-example"); + return FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/firebolt-response-get-tables-example"); + } + + private InputStream getInputStreamForGetSchemas() { + return FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/firebolt-response-get-schemas-example"); } private InputStream getInputStreamForGetVersion() { - return FireboltDatabaseMetadata.class - .getResourceAsStream("/responses/metadata/firebolt-response-get-version-example"); + return FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/firebolt-response-get-version-example"); } private InputStream getExpectedTypeInfo() { - InputStream is = FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/expected-types.csv"); + InputStream is = Objects.requireNonNull(FireboltDatabaseMetadata.class.getResourceAsStream("/responses/metadata/expected-types.csv")); String typesWithTabs = new BufferedReader( new InputStreamReader(is, StandardCharsets.UTF_8)) .lines() @@ -465,9 +896,14 @@ private InputStream getExpectedTypeInfo() { } - private void getFunctions(CheckedFunction getter) throws SQLException { + private void getFunctions(CheckedFunction getter, String ... examples) throws SQLException { String functions = getter.apply(fireboltDatabaseMetadata); assertNotNull(functions); - assertTrue(functions.length() > 0); + assertFalse(functions.isEmpty()); + Arrays.stream(examples).forEach(example -> assertTrue(functions.contains(example), example + " is not found in list")); + } + + private ResultSet createResultSet(InputStream is) throws SQLException { + return new FireboltResultSet(is, null, null, 65535, false, null, true); } } diff --git a/src/test/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadataTest.java b/src/test/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadataTest.java index 2ee12540..115b7a3f 100644 --- a/src/test/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadataTest.java +++ b/src/test/java/com/firebolt/jdbc/metadata/FireboltSystemEngineDatabaseMetadataTest.java @@ -1,37 +1,109 @@ package com.firebolt.jdbc.metadata; +import com.firebolt.jdbc.QueryResult; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.resultset.FireboltResultSet; +import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.testutils.AssertionUtil; +import com.firebolt.jdbc.type.FireboltDataType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.Callable; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_CATALOG; +import static com.firebolt.jdbc.metadata.MetadataColumns.TABLE_SCHEM; +import static java.lang.String.format; +import static java.util.regex.Pattern.CASE_INSENSITIVE; +import static java.util.regex.Pattern.compile; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.matches; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +@ExtendWith(MockitoExtension.class) class FireboltSystemEngineDatabaseMetadataTest { - private final FireboltSystemEngineDatabaseMetadata fireboltSystemEngineDatabaseMetadata = new FireboltSystemEngineDatabaseMetadata(null, null); + private static final List schemaColumns = List.of( + QueryResult.Column.builder().name(TABLE_SCHEM).type(FireboltDataType.TEXT).build(), + QueryResult.Column.builder().name(TABLE_CATALOG).type(FireboltDataType.TEXT).build()); + private static final String QUERY_TEMPLATE = "SELECT.+FROM information_schema.%ss.+WHERE.+table_schema like '%s'"; + @Mock + private FireboltConnection fireboltConnection; - @Test - void shouldReturnEmptyResultSetWhenGettingTable() throws SQLException { - ResultSet resultSet = fireboltSystemEngineDatabaseMetadata.getTables(null, null, null, null); - AssertionUtil.assertResultSetEquality(FireboltResultSet.empty(), resultSet); + @Mock + private FireboltStatement statement; + + private DatabaseMetaData fireboltSystemEngineDatabaseMetadata; + + @BeforeEach + void init() throws SQLException { + fireboltSystemEngineDatabaseMetadata = new FireboltSystemEngineDatabaseMetadata("jdbc:firebolt:host", fireboltConnection); + lenient().when(fireboltConnection.createStatement()).thenReturn(statement); + lenient().when(fireboltConnection.getCatalog()).thenReturn("db_name"); + lenient().when(fireboltConnection.getSessionProperties()).thenReturn(FireboltProperties.builder().database("my-db").principal("the-user").build()); } @Test - void shouldReturnEmptyResultSetWhenGettingColumns() throws SQLException { - ResultSet resultSet = fireboltSystemEngineDatabaseMetadata.getColumns(null, null, null, null); - AssertionUtil.assertResultSetEquality(FireboltResultSet.empty(), resultSet); + void getSchemas() throws Exception { + getSchemas(() -> fireboltSystemEngineDatabaseMetadata.getSchemas()); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"information_schema", "INFORMATION_SCHEMA", "info%"}) + void getSchemasWithSchemaPattern(String schemaPattern) throws Exception { + getSchemas(() -> fireboltSystemEngineDatabaseMetadata.getSchemas(null, schemaPattern)); } @Test - void shouldReturnEmptyResultSetWhenGettingSchemas() throws SQLException { - ResultSet resultSet = fireboltSystemEngineDatabaseMetadata.getSchemas(); - AssertionUtil.assertResultSetEquality(FireboltResultSet.empty(), resultSet); + void getSchemasWrongSchemaPattern() throws Exception { + ResultSet mockedRs = FireboltResultSet.of(QueryResult.builder().columns(schemaColumns).build()); + ResultSet expectedRs = FireboltResultSet.of(QueryResult.builder().columns(schemaColumns).build()); + when(statement.executeQuery(matches(compile(format(QUERY_TEMPLATE, "table", "does_not_exist"), CASE_INSENSITIVE)))).thenReturn(mockedRs); + AssertionUtil.assertResultSetEquality(expectedRs, fireboltSystemEngineDatabaseMetadata.getSchemas(null, "public")); + } + + private void getSchemas(Callable schemasGetter) throws Exception { + ResultSet mockedRs = FireboltResultSet.of(QueryResult.builder().columns(schemaColumns).rows(List.of(List.of("information_schema", "my-catalog"))).build()); + ResultSet expectedRs = FireboltResultSet.of(QueryResult.builder().columns(schemaColumns).rows(List.of(List.of("information_schema", "my-catalog"))).build()); + when(statement.executeQuery(matches(compile(format(QUERY_TEMPLATE, "table", "information_schema"), CASE_INSENSITIVE)))).thenReturn(mockedRs); + AssertionUtil.assertResultSetEquality(expectedRs, schemasGetter.call()); + } + + @ParameterizedTest + @ValueSource(strings = {"information_schema", "INFORMATION_SCHEMA", "info%"}) + void shouldReturnEmptyResultSetWhenGettingTable(String schemaPattern) throws Exception { + getEntities(() -> fireboltSystemEngineDatabaseMetadata.getTables(null, schemaPattern, null, null), "table", "information_schema"); + } + + @ParameterizedTest + @ValueSource(strings = {"information_schema", "INFORMATION_SCHEMA", "info%"}) + void shouldReturnEmptyResultSetWhenGettingColumns(String schemaPattern) throws Exception { + getEntities(() -> fireboltSystemEngineDatabaseMetadata.getColumns(null, schemaPattern, null, null), "column", "information_schema"); } @Test void isReadOnly() throws SQLException { - assertTrue(fireboltSystemEngineDatabaseMetadata.isReadOnly()); + assertFalse(fireboltSystemEngineDatabaseMetadata.isReadOnly()); } + + private void getEntities(Callable getter, String entity, String schemaPattern) throws Exception { + ResultSet rs = mock(ResultSet.class); + when(statement.executeQuery(matches(compile(format(QUERY_TEMPLATE, entity, schemaPattern), CASE_INSENSITIVE)))).thenReturn(rs); + assertNotNull(getter.call()); + } + } \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/metadata/MetadataUtilTest.java b/src/test/java/com/firebolt/jdbc/metadata/MetadataUtilTest.java index 5d416331..27bd2e5a 100644 --- a/src/test/java/com/firebolt/jdbc/metadata/MetadataUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/metadata/MetadataUtilTest.java @@ -22,28 +22,21 @@ class MetadataUtilTest { @Test void shouldGetTablesQueryWhenGettingQueryWithArguments() { assertEquals( - "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_schema LIKE 'db' AND table_name LIKE 'tableName' AND table_type NOT LIKE 'EXTERNAL' order by table_schema, table_name", - MetadataUtil.getTablesQuery("catalog", "db", "tableName")); + "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type IN ('FACT', 'DIMENSION') AND table_schema LIKE 'db' AND table_name LIKE 'tableName' order by table_schema, table_name", + MetadataUtil.getTablesQuery("catalog", "db", "tableName", new String[] {"FACT", "DIMENSION"})); } - @Test - void shouldGetTablesQueryWhenGettingQueryWithoutArguments() { + void shouldGetViewQueryWhenGettingQueryWithArguments() { assertEquals( - "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type NOT LIKE 'EXTERNAL' order by table_schema, table_name", - MetadataUtil.getTablesQuery(null, null, null)); + "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type IN ('VIEW') AND table_schema LIKE 'db' AND table_name LIKE 'tableName' order by table_schema, table_name", + MetadataUtil.getTablesQuery("catalog", "db", "tableName", new String[] {"VIEW"})); } @Test - void shouldGetViewsQueryWhenGettingQueryWithArguments() { + void shouldGetTablesQueryWhenGettingQueryWithoutArguments() { assertEquals( - "SELECT table_schema, table_name FROM information_schema.views WHERE table_schema LIKE 'schem' AND table_name LIKE 'tableName' order by table_schema, table_name", - MetadataUtil.getViewsQuery("catalog", "schem", "tableName")); - } - - @Test - void shouldGetViewsQueryWhenGettingQueryWithoutArguments() { - assertEquals("SELECT table_schema, table_name FROM information_schema.views order by table_schema, table_name", - MetadataUtil.getViewsQuery(null, null, null)); + "SELECT table_schema, table_name, table_type FROM information_schema.tables WHERE table_type IN ('FACT', 'DIMENSION', 'VIEW') order by table_schema, table_name", + MetadataUtil.getTablesQuery(null, null, null, new String[] {"FACT", "DIMENSION", "VIEW"})); } @Test @@ -59,10 +52,4 @@ void shouldGetColumnsQueryWhenGettingQueryWithoutArguments() { "SELECT table_schema, table_name, column_name, data_type, column_default, is_nullable, ordinal_position FROM information_schema.columns", MetadataUtil.getColumnsQuery(null, null, null)); } - - @Test - void shouldGetVersionQuery() { - assertEquals("SELECT version FROM information_schema.engines WHERE engine_name iLIKE 'test%'", - MetadataUtil.getDatabaseVersionQuery("test")); - } } diff --git a/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaDataTest.java b/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaDataTest.java index d8a852b1..b67061f1 100644 --- a/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaDataTest.java +++ b/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetMetaDataTest.java @@ -1,8 +1,10 @@ package com.firebolt.jdbc.resultset; -import static java.sql.ResultSetMetaData.columnNoNulls; -import static java.sql.ResultSetMetaData.columnNullable; -import static org.junit.jupiter.api.Assertions.*; +import com.firebolt.jdbc.CheckedFunction; +import com.firebolt.jdbc.resultset.column.Column; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.sql.ResultSet; import java.sql.ResultSetMetaData; @@ -11,11 +13,14 @@ import java.util.Arrays; import java.util.List; -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.Test; - -import com.firebolt.jdbc.resultset.column.Column; -import org.junit.jupiter.params.ParameterizedTest; +import static java.lang.String.format; +import static java.sql.ResultSetMetaData.columnNoNulls; +import static java.sql.ResultSetMetaData.columnNullable; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class FireboltResultSetMetaDataTest { @@ -46,14 +51,14 @@ void shouldReturnTrueWhenColumnNotSigned() throws SQLException { @Test void shouldReturnColumnNameAndLabel() throws SQLException { - FireboltResultSetMetaData fireboltResultSetMetaData = getMetaData(); + ResultSetMetaData fireboltResultSetMetaData = getMetaData(); assertEquals("name", fireboltResultSetMetaData.getColumnName(1)); assertEquals("name", fireboltResultSetMetaData.getColumnLabel(1)); } @Test void shouldReturnEmptyWhenGettingSchemaAsItIsNotSupported() throws SQLException { - assertEquals(StringUtils.EMPTY, getMetaData().getSchemaName(1)); + assertEquals("", getMetaData().getSchemaName(1)); } @Test @@ -68,12 +73,12 @@ void shouldReturnPrecision() throws SQLException { @Test void shouldReturnTableName() throws SQLException { - assertEquals("table-name", getMetaData().getTableName()); + assertEquals("table-name", getMetaData().getTableName(1)); } @Test void shouldReturnDbName() throws SQLException { - assertEquals("db-name", getMetaData().getDbName()); + assertEquals("db-name", getMetaData().getCatalogName(1)); } @Test @@ -93,21 +98,57 @@ void shouldReturnColumnClassName() throws SQLException { @Test void shouldReturnTrueWhenColumnIsCaseSensitiveAndFalseOtherwise() throws SQLException { - FireboltResultSetMetaData fireboltResultSetMetaData = getMetaData(); + ResultSetMetaData fireboltResultSetMetaData = getMetaData(); assertTrue(fireboltResultSetMetaData.isCaseSensitive(1)); assertFalse(fireboltResultSetMetaData.isCaseSensitive(2)); } @Test - void isReadOnly() throws SQLException { - FireboltResultSetMetaData fireboltResultSetMetaData = FireboltResultSetMetaData.builder().columns(getColumns()) - .tableName("table-name").dbName("db-name").build(); - assertTrue(fireboltResultSetMetaData.isReadOnly(1)); + void trivialMarkers() throws SQLException { + ResultSetMetaData md = getMetaData(); + assertTrue(md.isReadOnly(1)); + assertFalse(md.isAutoIncrement(1)); + assertTrue(md.isSearchable(1)); + assertFalse(md.isCurrency(1)); + assertEquals(80, md.getColumnDisplaySize(1)); + assertEquals("", md.getSchemaName(1)); + assertFalse(md.isWritable(1)); + assertFalse(md.isDefinitelyWritable(1)); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 0, 4}) + void wrongIndex(int column) { + ResultSetMetaData md = getMetaData(); + assertFunctionThrowsWrongIndex(md::isNullable, column); + assertFunctionThrowsWrongIndex(md::isSigned, column); + assertFunctionThrowsWrongIndex(md::getColumnLabel, column); + assertFunctionThrowsWrongIndex(md::getColumnName, column); + assertFunctionThrowsWrongIndex(md::getPrecision, column); + assertFunctionThrowsWrongIndex(md::getScale, column); + assertFunctionThrowsWrongIndex(md::getTableName, column); + assertFunctionThrowsWrongIndex(md::getCatalogName, column); + assertFunctionThrowsWrongIndex(md::getColumnType, column); + assertFunctionThrowsWrongIndex(md::getColumnTypeName, column); + assertFunctionThrowsWrongIndex(md::getColumnClassName, column); + assertFunctionThrowsWrongIndex(md::isCaseSensitive, column); + assertFunctionThrowsWrongIndex(md::isAutoIncrement, column); + assertFunctionThrowsWrongIndex(md::isSearchable, column); + assertFunctionThrowsWrongIndex(md::isCurrency, column); + assertFunctionThrowsWrongIndex(md::getColumnDisplaySize, column); + assertFunctionThrowsWrongIndex(md::getSchemaName, column); + assertFunctionThrowsWrongIndex(md::isReadOnly, column); + assertFunctionThrowsWrongIndex(md::isWritable, column); + assertFunctionThrowsWrongIndex(md::isDefinitelyWritable, column); + } + + private void assertFunctionThrowsWrongIndex(CheckedFunction function, int column) { + assertEquals(format("Invalid column number %d", column), assertThrows(SQLException.class, () -> function.apply(column)).getMessage()); } @Test void wrap() throws SQLException { - FireboltResultSetMetaData fireboltResultSetMetaData = getMetaData(); + ResultSetMetaData fireboltResultSetMetaData = getMetaData(); assertTrue(fireboltResultSetMetaData.isWrapperFor(ResultSetMetaData.class)); assertSame(fireboltResultSetMetaData, fireboltResultSetMetaData.unwrap(ResultSetMetaData.class)); @@ -120,7 +161,7 @@ private List getColumns() { Column.of("Decimal(1,2)", "Weight")); } - private FireboltResultSetMetaData getMetaData() { - return FireboltResultSetMetaData.builder().columns(getColumns()).tableName("table-name").dbName("db-name").build(); + private ResultSetMetaData getMetaData() { + return new FireboltResultSetMetaData("db-name", "table-name", getColumns()); } } diff --git a/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetTest.java b/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetTest.java index a3b949b8..5d3c7e3e 100644 --- a/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetTest.java +++ b/src/test/java/com/firebolt/jdbc/resultset/FireboltResultSetTest.java @@ -1,32 +1,70 @@ package com.firebolt.jdbc.resultset; +import com.firebolt.jdbc.CheckedFunction; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.util.LoggerUtil; -import org.apache.commons.lang3.StringUtils; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.function.Executable; import org.junitpioneer.jupiter.DefaultTimeZone; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import java.math.BigDecimal; import java.math.BigInteger; import java.math.RoundingMode; -import java.sql.*; +import java.net.MalformedURLException; +import java.net.URL; +import java.sql.Array; +import java.sql.Clob; +import java.sql.Date; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.sql.Wrapper; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.time.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Calendar; +import java.util.Map; import java.util.TimeZone; +import java.util.concurrent.Callable; +import static com.firebolt.jdbc.exception.ExceptionType.TYPE_TRANSFORMATION_ERROR; +import static java.lang.String.format; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @DefaultTimeZone("UTC") @@ -50,32 +88,226 @@ void afterEach() throws SQLException, IOException { void shouldReturnMetadata() throws SQLException { // This only tests that Metadata is available with the resultSet. inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, false); + resultSet = createResultSet(inputStream); assertNotNull(resultSet.getMetaData()); - assertEquals("any_name", resultSet.getMetaData().getTableName(1)); - assertEquals("array_db", resultSet.getMetaData().getCatalogName(1)); + assertEquals("a_table", resultSet.getMetaData().getTableName(1)); + assertEquals("a_db", resultSet.getMetaData().getCatalogName(1)); + } + + @Test + void attributes() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + assertEquals(ResultSet.CONCUR_READ_ONLY, resultSet.getConcurrency()); + assertEquals(ResultSet.FETCH_FORWARD, resultSet.getFetchDirection()); + assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, resultSet.getHoldability()); + } + + @Test + void setFetchDirection() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.setFetchDirection(ResultSet.FETCH_FORWARD); // should just work + assertThrows(SQLException.class, () -> resultSet.setFetchDirection(ResultSet.FETCH_REVERSE)); + assertThrows(SQLException.class, () -> resultSet.setFetchDirection(ResultSet.FETCH_UNKNOWN)); + } + + @Test + void getRow() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + int i = 0; + do { + assertEquals(i, resultSet.getRow()); + i++; + } while (resultSet.next()); + } + + @Test + void getStatement() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + assertEquals(fireboltStatement, resultSet.getStatement()); + } + + @Test + void unsupportedNavigation() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + + assertThrowsForwardOnly("first", () -> resultSet.first()); + assertThrowsForwardOnly("last", () -> resultSet.last()); + assertThrowsForwardOnly("beforeFirst", () -> {resultSet.beforeFirst(); return null;}); + assertThrowsForwardOnly("afterLast", () -> {resultSet.afterLast(); return null;}); + assertThrowsForwardOnly("absolute", () -> resultSet.absolute(1)); + assertThrowsForwardOnly("relative", () -> resultSet.relative(1)); + assertThrowsForwardOnly("previous", () -> resultSet.previous()); + } + + private void assertThrowsForwardOnly(String name, Callable method) { + assertEquals(format("Cannot call %s() for ResultSet of type TYPE_FORWARD_ONLY", name), assertThrows(SQLException.class, method::call).getMessage()); + } + + @Test + void unsupported() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.getCursorName()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.rowUpdated()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.rowDeleted()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.rowInserted()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.getRowId(1)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.getRowId("no-name")); + + // updates + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNull(1)); + + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBoolean(1, true)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateByte(1, (byte)0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateShort(1, (short)0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateInt(1, 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateLong(1, 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateFloat(1, 0.0f)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateDouble(1, 0.0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBigDecimal(1, new BigDecimal(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateString(1, "")); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBytes(1, new byte[0])); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateDate(1, new Date(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateTime(1, new Time(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateTimestamp(1, new Timestamp(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream(1, new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream(1, new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream(1, new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream(1, new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream(1, new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream(1, new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateObject(1, null, 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateObject(1, null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNull("label")); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBoolean("label", true)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateByte("label", (byte)0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateShort("label", (short)0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateInt("label", 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateLong("label", 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateFloat("label", 0.0f)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateDouble("label", 0.0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBigDecimal("label", new BigDecimal(0))); + + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateString("label", "")); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBytes("label", new byte[0])); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateDate("label", new Date(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateTime("label", new Time(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateTimestamp("label", new Timestamp(0))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream("label", new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream("label", new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream("label", new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream("label", new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream("label", new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateObject("label", null, 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateObject("label", null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.insertRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.deleteRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.refreshRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.cancelRowUpdates()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.moveToInsertRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.moveToCurrentRow()); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateRef(1, null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateRef("label", null)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob(1, mock(java.sql.Blob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob("label", mock(java.sql.Blob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob(1, mock(Clob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob("label", mock(Clob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateArray(1, mock(Array.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateArray("label", mock(Array.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateRowId(1, mock(RowId.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateRowId("label", mock(RowId.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNString(1, "")); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNString("label", "")); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob(1, mock(java.sql.NClob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob("label", mock(java.sql.NClob.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateSQLXML(1, mock(SQLXML.class))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateSQLXML("", mock(SQLXML.class))); + + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNCharacterStream(1, new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNCharacterStream("label", new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream(1, new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream(1, new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream("label", new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream("label", new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream("label", new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream("label", new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream("label", new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream("label", new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob(1, new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob("label", new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob(1, new ByteArrayInputStream(new byte[0]), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob(1, new ByteArrayInputStream(new byte[0]), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob(1, new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob(1, new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob("label", new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob("label", new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob(1, new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob(1, new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob("label", new StringReader(""), 0)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob("label", new StringReader(""), 0L)); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNCharacterStream(1, new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNCharacterStream("label", new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream(1, new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream(1, new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream(1, new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateAsciiStream("label", new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBinaryStream("label", new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateCharacterStream("label", new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob(1, new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateBlob("label", new ByteArrayInputStream(new byte[0]))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob(1, new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateClob("label", new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob(1, new StringReader(""))); + assertThrows(SQLFeatureNotSupportedException.class, () -> resultSet.updateNClob("label", new StringReader(""))); + } + + @Test + void fetchSize() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + assertEquals(0, resultSet.getFetchSize()); + + resultSet.setFetchSize(0); // ignored + resultSet.setFetchSize(1); // ignored + assertThrows(SQLException.class, () -> resultSet.setFetchSize(-1)); } @Test void shouldNotBeLastWhenThereIsMoreData() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertFalse(resultSet.isLast()); } @Test void shouldNotBeLastAtLastLine() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); assertTrue(resultSet.isLast()); } + @Test + void maxRows() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + when(fireboltStatement.getMaxRows()).thenReturn(1); + resultSet = createResultSet(inputStream); + assertTrue(resultSet.next()); + assertFalse(resultSet.next()); // the result has 2 rows but maxRows=1, so the second next() returns false + } + @Test void shouldReadAllTheData() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(1, resultSet.getObject(1)); String[][][] firstArray = { { { "1", "2" }, { "3", "4" } } }; @@ -92,16 +324,16 @@ void shouldReadAllTheData() throws SQLException { @Test void shouldBeBeforeFirstIfFirstRowNotRead() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertTrue(resultSet.isBeforeFirst()); resultSet.next(); assertFalse(resultSet.isBeforeFirst()); } @Test - void shouldGetBigDecimal() throws SQLException { + void shouldGetBigDecimalSimple() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(new BigDecimal("1"), resultSet.getBigDecimal(1)); assertEquals(new BigDecimal("1"), resultSet.getBigDecimal("id")); @@ -111,16 +343,26 @@ void shouldGetBigDecimal() throws SQLException { @SuppressWarnings("deprecation") // ResultSet.getBigDecimal() is deprecated but still has to be tested. void shouldGetBigDecimalWithScale() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(new BigDecimal("1").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal(1, 2)); assertEquals(new BigDecimal("1").setScale(2, RoundingMode.HALF_UP), resultSet.getBigDecimal("id", 2)); } + @Test + void shouldGetBigDecimalNull() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.next(); + resultSet.next(); + assertNull(resultSet.getBigDecimal(7)); + assertNull(resultSet.getBigDecimal("an_integer")); + } + @Test void shouldBeFirstWhenNextRecordIsTheFirstToRead() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertTrue(resultSet.isFirst()); resultSet.next(); @@ -130,7 +372,7 @@ void shouldBeFirstWhenNextRecordIsTheFirstToRead() throws SQLException { @Test void shouldBeAfterReadingTheLast() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertFalse(resultSet.isAfterLast()); assertFalse(resultSet.isLast()); while (resultSet.next()) { @@ -144,7 +386,7 @@ void shouldBeAfterReadingTheLast() throws SQLException { @Test void shouldReturnFalseWhenCallingWasNullAfterRead() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.getInt(1); assertFalse(resultSet.wasNull()); @@ -153,7 +395,7 @@ void shouldReturnFalseWhenCallingWasNullAfterRead() throws SQLException { @Test void shouldThrowExceptionWhenCallingWasNullBeforeAnyGet() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertThrows(IllegalArgumentException.class, () -> resultSet.wasNull(), "A column must be read before checking nullability"); @@ -163,7 +405,7 @@ void shouldThrowExceptionWhenCallingWasNullBeforeAnyGet() throws SQLException { void shouldReturnTrueWhenLastValueGotWasNull() throws SQLException { // This only tests that Metadata is available with the resultSet. inputStream = getInputStreamWithNulls(); - resultSet = new FireboltResultSet(inputStream); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.getObject(2); assertTrue(resultSet.wasNull()); @@ -171,42 +413,58 @@ void shouldReturnTrueWhenLastValueGotWasNull() throws SQLException { assertTrue(resultSet.wasNull()); } + @Test + void shouldThrowIfTypeIsNull() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.next(); + assertEquals("The type provided is null", assertThrows(SQLException.class, () -> resultSet.getObject(1, (Class)null)).getMessage()); + } + @Test void shouldReturnInt() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(1, resultSet.getInt(1)); assertEquals(1, resultSet.getInt("id")); assertEquals(1, resultSet.getObject(1, Long.class)); + assertEquals(1L, resultSet.getObject(1, Map.of("int", Long.class))); + assertEquals(1L, resultSet.getObject("id", Map.of("INTEGER", Long.class))); + assertEquals(1., resultSet.getObject(1, Map.of("int32", Double.class))); + assertThrows(SQLException.class, () -> resultSet.getObject(1, Map.of("real", Double.class))); // exising type that does not match column type + assertThrows(SQLException.class, () -> resultSet.getObject(1, Map.of("notatype", Double.class))); // type alias that does not exist resultSet.next(); assertEquals(2, resultSet.getInt(1)); assertEquals(2, resultSet.getInt("id")); assertEquals(2, resultSet.getObject(1, Long.class)); + assertEquals(2, resultSet.getLong(1)); + assertEquals(2, resultSet.getLong("id")); } @Test void shouldReturnFloat() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "a_table", "a_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(14.6f, resultSet.getFloat(6)); assertEquals(14.6f, resultSet.getFloat("a_double")); assertEquals(14.6f, resultSet.getObject(6, Float.class)); + assertEquals(14.6, resultSet.getObject(6, Map.of("Float32", Double.class))); + assertEquals((short)14, resultSet.getObject(6, Map.of("Float32", Short.class))); resultSet.next(); assertEquals(0, resultSet.getFloat(6)); assertEquals(0, resultSet.getFloat("a_double")); assertNull(resultSet.getObject(6, Float.class)); - } @Test void shouldReturnDouble() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "a_table", "a_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(14.6d, resultSet.getDouble(6)); assertEquals(14.6d, resultSet.getDouble("a_double")); @@ -218,19 +476,28 @@ void shouldReturnDouble() throws SQLException { @Test void shouldReturnString() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); String expected = "Taylor's Prime Steak House"; assertEquals(expected, resultSet.getString(3)); assertEquals(expected, resultSet.getString("name")); assertEquals(expected, resultSet.getObject(3, String.class)); + assertEquals(expected, resultSet.getNString(3)); + assertEquals(expected, resultSet.getNString("name")); + assertEquals(expected, getStringFromClob(resultSet.getClob(3))); + assertEquals(expected, getStringFromClob(resultSet.getClob("name"))); + assertEquals(expected, getStringFromClob(resultSet.getNClob(3))); + assertEquals(expected, getStringFromClob(resultSet.getNClob("name"))); + } + private String getStringFromClob(Clob clob) throws SQLException { + return clob.getSubString(1, (int)clob.length()); } @Test void shouldReturnShort() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(5, resultSet.getShort("an_integer")); assertEquals(5, resultSet.getShort(7)); @@ -242,37 +509,43 @@ void shouldReturnShort() throws SQLException { @Test void shouldReturnTypeForward() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertEquals(TYPE_FORWARD_ONLY, resultSet.getType()); } @Test - void shouldReturnBytes() throws SQLException { + void shouldReturnBytes() throws SQLException, IOException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); byte[] expected = "Taylor\\'s Prime Steak House".getBytes(); assertArrayEquals(expected, resultSet.getBytes(3)); assertArrayEquals(expected, resultSet.getBytes("name")); + assertArrayEquals(expected, resultSet.getBinaryStream(3).readAllBytes()); + assertArrayEquals(expected, resultSet.getBinaryStream("name").readAllBytes()); assertArrayEquals(expected, resultSet.getObject(3, byte[].class)); assertArrayEquals(expected, resultSet.getObject("name", byte[].class)); + assertArrayEquals(expected, resultSet.getBlob(3).getBinaryStream().readAllBytes()); + assertArrayEquals(expected, resultSet.getBlob("name").getBinaryStream().readAllBytes()); resultSet.next(); assertNull(resultSet.getBytes(3)); + assertNull(resultSet.getBlob(3)); } @Test void shouldReturnNullWhenValueIsNull() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); assertNull(resultSet.getBytes(3)); + assertNull(resultSet.getBlob(3)); } @Test void shouldReturnByte() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(1, resultSet.getByte(1)); assertEquals(1, resultSet.getByte("id")); @@ -281,7 +554,7 @@ void shouldReturnByte() throws SQLException { @Test void shouldReturn0ByteWhenValueIsNull() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); assertEquals((byte) 0, resultSet.getByte(3)); @@ -290,18 +563,24 @@ void shouldReturn0ByteWhenValueIsNull() throws SQLException { @Test void shouldReturnNullWhenValueStringIsNull() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); // second line contains \N which represents a null value assertNull(resultSet.getString(3)); assertNull(resultSet.getString("name")); + assertNull(resultSet.getNString(3)); + assertNull(resultSet.getNString("name")); + assertNull(resultSet.getClob(3)); + assertNull(resultSet.getClob("name")); + assertNull(resultSet.getNClob(3)); + assertNull(resultSet.getNClob("name")); } @Test void shouldReturnDate() throws SQLException { Date expectedDate = Date.valueOf(LocalDate.of(2022, 5, 10)); inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedDate, resultSet.getDate(4)); @@ -315,7 +594,7 @@ void shouldReturnTimeStamp() throws SQLException, ParseException { Timestamp timestamp = new Timestamp(parsedDate.getTime()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(timestamp, resultSet.getTimestamp(2)); @@ -325,7 +604,7 @@ void shouldReturnTimeStamp() throws SQLException, ParseException { @Test void shouldReturnBoolean() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertTrue(resultSet.getBoolean(5)); @@ -340,7 +619,7 @@ void shouldReturnTime() throws SQLException { Time expectedTime = new Time( ZonedDateTime.of(1970, 1, 1, 13, 1, 2, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedTime, resultSet.getTime(2)); assertEquals(expectedTime, resultSet.getTime("a_datetime")); @@ -349,7 +628,7 @@ void shouldReturnTime() throws SQLException { @Test void shouldGetArray() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); String[][][] firstArray = { { { "1", "2" }, { "3", "4" } } }; assertArrayEquals(firstArray, ((String[][][]) (resultSet.getArray("arr")).getArray())); @@ -360,7 +639,7 @@ void shouldGetArray() throws SQLException { void shouldReturnUnescapedString() throws SQLException { String expected = "[0] [Aggregate] GroupBy: [] Aggregates: [COUNT(DISTINCT FB_NODE_2.a1), APPROX_COUNT_DISTINCT(FB_NODE_2.a1)] @ FB_NODE_1\n \\_[1] [StoredTable] Name: 'ft', used 1/1 column(s) FACT @ FB_NODE_2\n"; inputStream = getInputStreamWitExplain(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expected, resultSet.getString(1)); assertEquals(expected, resultSet.getObject(1)); @@ -369,7 +648,7 @@ void shouldReturnUnescapedString() throws SQLException { @Test void shouldThrowException() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "empty_test", "empty_test", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); // second line contains \N which represents a null value assertNull(resultSet.getString(3)); @@ -379,7 +658,7 @@ void shouldThrowException() throws SQLException { @Test void shouldThrowExceptionWhenCheckingWasNullAfterClose() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.close(); assertThrows(SQLException.class, resultSet::wasNull); } @@ -387,7 +666,7 @@ void shouldThrowExceptionWhenCheckingWasNullAfterClose() throws SQLException { @Test void shouldThrowExceptionWhenGettingValueAfterClose() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.close(); assertThrows(SQLException.class, () -> resultSet.getObject(1)); } @@ -395,7 +674,7 @@ void shouldThrowExceptionWhenGettingValueAfterClose() throws SQLException { @Test void shouldThrowSQLExceptionWhenGettingValueWithInvalidColumnIndex() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.close(); assertThrows(SQLException.class, () -> resultSet.getObject("INVALID_COLUMN")); } @@ -403,7 +682,7 @@ void shouldThrowSQLExceptionWhenGettingValueWithInvalidColumnIndex() throws SQLE @Test void shouldCloseStream() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertFalse(resultSet.isClosed()); resultSet.close(); assertTrue(resultSet.isClosed()); @@ -413,7 +692,7 @@ void shouldCloseStream() throws SQLException { void shouldCloseStatementWhenCloseOnCompletion() throws SQLException { when(fireboltStatement.isCloseOnCompletion()).thenReturn(true); inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, fireboltStatement); + resultSet = createResultSet(inputStream); resultSet.close(); verify(fireboltStatement).close(); } @@ -422,7 +701,9 @@ void shouldCloseStatementWhenCloseOnCompletion() throws SQLException { void shouldNotCloseStatementWhenNotCloseOnCompletion() throws SQLException { when(fireboltStatement.isCloseOnCompletion()).thenReturn(false); inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, fireboltStatement); + when(fireboltStatement.getMaxRows()).thenReturn(1024); + when(fireboltStatement.getMaxFieldSize()).thenReturn(0); + resultSet = createResultSet(inputStream); resultSet.close(); verifyNoMoreInteractions(fireboltStatement); } @@ -430,7 +711,7 @@ void shouldNotCloseStatementWhenNotCloseOnCompletion() throws SQLException { @Test void shouldNotThrowExceptionWhenClosingTwice() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.close(); assertTrue(resultSet.isClosed()); try { @@ -443,17 +724,17 @@ void shouldNotThrowExceptionWhenClosingTwice() throws SQLException { @Test void shouldThrowExceptionWhenColumnDoesNotExist() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); assertThrows(SQLException.class, () -> resultSet.getObject(50)); } @Test void shouldReturnEmptyWhenValueFoundIsEmpty() throws SQLException { inputStream = getInputStreamWithEmpty(); - resultSet = new FireboltResultSet(inputStream, "table_with_empty", "db_with_emtpy", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); - assertEquals(StringUtils.EMPTY, resultSet.getObject("name")); - assertEquals(StringUtils.EMPTY, resultSet.getObject("city")); + assertEquals("", resultSet.getObject("name")); + assertEquals("", resultSet.getObject("city")); } @Test @@ -462,8 +743,7 @@ void shouldLogResultSet() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); try (MockedStatic loggerUtilMockedStatic = mockStatic(LoggerUtil.class)) { loggerUtilMockedStatic.when(() -> LoggerUtil.logInputStream(inputStream)).thenReturn(inputStream); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, - true); + resultSet = createResultSet(inputStream); loggerUtilMockedStatic.verify(() -> LoggerUtil.logInputStream(inputStream)); } } @@ -473,8 +753,7 @@ void shouldGetTimeWithTimezoneFromCalendar() throws SQLException { inputStream = getInputStreamWithDates(); try (MockedStatic loggerUtilMockedStatic = mockStatic(LoggerUtil.class)) { loggerUtilMockedStatic.when(() -> LoggerUtil.logInputStream(inputStream)).thenReturn(inputStream); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, - true); + resultSet = createResultSet(inputStream); resultSet.next(); Time firstExpectedTime = new Time( @@ -495,8 +774,7 @@ void shouldGetTimestampWithTimezoneFromCalendar() throws SQLException { inputStream = getInputStreamWithDates(); try (MockedStatic loggerUtilMockedStatic = mockStatic(LoggerUtil.class)) { loggerUtilMockedStatic.when(() -> LoggerUtil.logInputStream(inputStream)).thenReturn(inputStream); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, - true); + resultSet = createResultSet(inputStream); resultSet.next(); Timestamp firstTimeStampFromEST = Timestamp .valueOf(ZonedDateTime.of(2022, 5, 10, 18, 1, 2, 0, UTC_TZ.toZoneId()).toLocalDateTime()); @@ -518,8 +796,7 @@ void shouldGetTimeObjectsWithTimeZoneFromResponse() throws SQLException { inputStream = getInputStreamWithDates(); try (MockedStatic loggerUtilMockedStatic = mockStatic(LoggerUtil.class)) { loggerUtilMockedStatic.when(() -> LoggerUtil.logInputStream(inputStream)).thenReturn(inputStream); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, - false); + resultSet = createResultSet(inputStream); resultSet.next(); ZonedDateTime zonedDateTime = ZonedDateTime.of(2022, 5, 10, 18, 1, 2, 0, UTC_TZ.toZoneId()); @@ -549,8 +826,7 @@ void shouldGetDateWithTimezoneFromCalendar() throws SQLException { inputStream = getInputStreamWithDates(); try (MockedStatic loggerUtilMockedStatic = mockStatic(LoggerUtil.class)) { loggerUtilMockedStatic.when(() -> LoggerUtil.logInputStream(inputStream)).thenReturn(inputStream); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535, false, fireboltStatement, - true); + resultSet = createResultSet(inputStream); resultSet.next(); Date firstExpectedDateFromEST = new Date(ZonedDateTime .of(2022, 5, 10, 5, 0, 0, 0, TimeZone.getTimeZone("UTC").toZoneId()).toInstant().toEpochMilli()); @@ -569,7 +845,7 @@ void shouldGetDateWithTimezoneFromCalendar() throws SQLException { @Test void shouldFindNullByteA() throws SQLException { inputStream = getInputStreamWithByteA(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertNull(resultSet.getObject("null_bytea")); } @@ -577,7 +853,7 @@ void shouldFindNullByteA() throws SQLException { @Test void shouldFindByteAWithValue() throws SQLException { inputStream = getInputStreamWithByteA(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertArrayEquals(new byte[] { -34, -83, -66, -17 }, (byte[]) resultSet.getObject("a_bytea")); assertEquals("\\xdeadbeef", resultSet.getString("a_bytea")); @@ -589,16 +865,25 @@ void shouldFindByteAWithValue() throws SQLException { @Test void shouldFindEmptyByteA() throws SQLException { inputStream = getInputStreamWithByteA(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertArrayEquals(new byte[] {}, (byte[]) resultSet.getObject("an_empty_bytea")); assertEquals("", resultSet.getString("an_empty_bytea")); } + @Test + void shouldThrowExceptionWhenCannotConvertToByteArray() throws SQLException { + inputStream = getInputStreamWithByteA(); + resultSet = createResultSet(inputStream); + while (resultSet.next()) { + assertEquals("Cannot convert binary string in non-hex format to byte array", assertThrows(SQLException.class, () -> resultSet.getObject("false_bytea")).getMessage()); + } + } + @Test void shouldReturnTrueWhenBooleanFoundIsTrue() throws SQLException { inputStream = getInputStreamWithBooleans(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertTrue((Boolean) resultSet.getObject("true_boolean")); assertTrue(resultSet.getBoolean("true_boolean")); @@ -611,7 +896,7 @@ void shouldReturnTrueWhenBooleanFoundIsTrue() throws SQLException { @Test void shouldReturnFalseWhenBooleanFoundIsFalse() throws SQLException { inputStream = getInputStreamWithBooleans(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertFalse((Boolean) resultSet.getObject("false_boolean")); assertFalse(resultSet.getBoolean("false_boolean")); @@ -624,7 +909,7 @@ void shouldReturnFalseWhenBooleanFoundIsFalse() throws SQLException { @Test void shouldReturnFalseWhenBooleanFoundIsNull() throws SQLException { inputStream = getInputStreamWithBooleans(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertNull(resultSet.getObject("null_boolean")); assertFalse(resultSet.getBoolean("null_boolean")); @@ -633,7 +918,7 @@ void shouldReturnFalseWhenBooleanFoundIsNull() throws SQLException { @Test void shouldThrowExceptionWhenBooleanValueIsInvalid() throws SQLException { inputStream = getInputStreamWithBooleans(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertThrows(FireboltException.class, () -> resultSet.getObject("invalid_boolean")); } @@ -644,7 +929,7 @@ void shouldReturnTimestampFromTimestampntz() throws SQLException { ZonedDateTime.of(2022, 5, 10, 23, 1, 2, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedTimestamp, resultSet.getTimestamp("timestampntz")); @@ -660,7 +945,7 @@ void shouldReturnDateFromTimestampntz() throws SQLException { ZonedDateTime.of(2022, 5, 10, 0, 0, 0, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedDate, resultSet.getDate("timestampntz")); @@ -677,7 +962,7 @@ void shouldReturnTimeFromTimestampntz() throws SQLException { Time expectedTimeFromEST = new Time( ZonedDateTime.of(1970, 1, 2, 4, 1, 2, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedTime, resultSet.getTime("timestampntz")); assertEquals(expectedTimeFromEST, resultSet.getTime("timestampntz", EST_CALENDAR)); @@ -690,7 +975,7 @@ void shouldReturnTimeFromTimestamptz() throws SQLException { Time expectedTimeFromEST = new Time( ZonedDateTime.of(1970, 1, 1, 6, 1, 2, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedTime, resultSet.getTime("timestamptz")); assertEquals(expectedTimeFromEST, resultSet.getTime("timestamptz", EST_CALENDAR)); @@ -702,7 +987,7 @@ void shouldReturnTimestampFromTimestamptz() throws SQLException { ZonedDateTime.of(2022, 5, 11, 6, 1, 2, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedTimestamp, resultSet.getTimestamp("timestamptz")); @@ -716,7 +1001,7 @@ void shouldReturnDateFromTimestamptz() throws SQLException { ZonedDateTime.of(2022, 5, 11, 0, 0, 0, 0, UTC_TZ.toZoneId()).toInstant().toEpochMilli()); inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(expectedDate, resultSet.getDate("timestamptz")); @@ -729,7 +1014,7 @@ void shouldReturnDateFromTimestamptz() throws SQLException { @Test void shouldReturnNullForTimeTypesWithNullValues() throws SQLException { inputStream = getInputStreamWithDates(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); resultSet.next(); assertNull(resultSet.getTimestamp("timestamptz")); @@ -743,7 +1028,7 @@ void shouldReturnNullForTimeTypesWithNullValues() throws SQLException { @Test void shouldReturnDataTypes() throws SQLException { inputStream = getInputStreamWithNewTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(Types.VARCHAR, resultSet.getMetaData().getColumnType(1)); assertEquals(Types.DATE, resultSet.getMetaData().getColumnType(2)); @@ -763,7 +1048,7 @@ void shouldReturnDataTypes() throws SQLException { @Test void shouldReturnDataForNewNonNumericDataTypes() throws SQLException { inputStream = getInputStreamWithNewTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals("text", resultSet.getObject(1)); assertEquals(Date.valueOf(LocalDate.of(1, 3, 28)), resultSet.getObject(2)); @@ -780,26 +1065,207 @@ void shouldReturnDataForNewNonNumericDataTypes() throws SQLException { } @Test - void shouldGetObjectsForNumericTypes() throws SQLException { + void shouldGetByte() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals((byte)1, resultSet.getObject(1, Byte.class)); + assertEquals((byte)1, resultSet.getByte(1)); + + assertTransformationError(2, Byte.class); + assertTransformationError(2, i -> resultSet.getByte(i)); + + assertEquals((byte)1, resultSet.getObject(3, Byte.class)); + assertEquals((byte)1, resultSet.getByte(3)); + + assertEquals((byte)1, resultSet.getObject(4, Byte.class)); + assertEquals((byte)1, resultSet.getByte(4)); + + assertTransformationError(5, Byte.class); + assertTransformationError(5, i -> resultSet.getByte(i)); + + assertTransformationError(6, Byte.class); + assertTransformationError(6, i -> resultSet.getByte(i)); + } + + @Test + void shouldGetShort() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + assertEquals((short)1, resultSet.getObject(1, Short.class)); + assertEquals((short)1, resultSet.getShort(1)); + + assertTransformationError(2, Short.class); + assertTransformationError(2, i -> resultSet.getShort(i)); + + assertEquals((short)1, resultSet.getObject(3, Short.class)); + assertEquals((short)1, resultSet.getShort(3)); + + assertEquals((short)1, resultSet.getObject(4, Short.class)); + assertEquals((short)1, resultSet.getShort(4)); + + assertTransformationError(5, Short.class); + assertTransformationError(5, i -> resultSet.getShort(i)); + + assertEquals((short)30000, resultSet.getObject(6, Short.class)); + assertEquals((short)30000, resultSet.getShort(6)); + } + + @Test + void shouldGetInt() throws SQLException { inputStream = getInputStreamWithNumericTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(1, resultSet.getObject(1, Integer.class)); - assertEquals(new BigInteger("1"), resultSet.getObject(1, BigInteger.class)); - assertEquals(1, resultSet.getObject(1, Long.class)); + assertEquals(1, resultSet.getInt(1)); + + assertTransformationError(2, Integer.class); + assertTransformationError(2, i -> resultSet.getInt(i)); + + assertEquals(1, resultSet.getObject(3, Integer.class)); + assertEquals(1, resultSet.getInt(3)); + + assertEquals(1, resultSet.getObject(4, Integer.class)); + assertEquals(1, resultSet.getInt(4)); + + assertEquals(1231232, resultSet.getObject(5, Integer.class)); + assertEquals(1231232, resultSet.getInt(5)); + + assertEquals(30000, resultSet.getObject(6, Integer.class)); + assertEquals(30000, resultSet.getInt(6)); + } + + @Test + void shouldGetLong() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(1L, resultSet.getObject(1, Long.class)); + assertEquals(1L, resultSet.getLong(1)); + assertEquals(30000000000L, resultSet.getObject(2, Long.class)); + assertEquals(30000000000L, resultSet.getLong(2)); + + assertEquals(1, resultSet.getObject(3, Long.class)); + assertEquals(1, resultSet.getLong(3)); + + assertEquals(1, resultSet.getObject(4, Long.class)); + assertEquals(1, resultSet.getLong(4)); + + assertEquals(1231232L, resultSet.getObject(5, Long.class)); + assertEquals(1231232L, resultSet.getLong(5)); + + assertEquals(30000L, resultSet.getObject(6, Long.class)); + assertEquals(30000L, resultSet.getLong(6)); + } + + @Test + void shouldGetBigInteger() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(new BigInteger("1"), resultSet.getObject(1, BigInteger.class)); assertEquals(new BigInteger("30000000000"), resultSet.getObject(2, BigInteger.class)); + assertEquals(new BigInteger("1"), resultSet.getObject(3, BigInteger.class)); + assertEquals(new BigInteger("1"), resultSet.getObject(4, BigInteger.class)); + assertEquals(new BigInteger("1231232"), resultSet.getObject(5, BigInteger.class)); + assertEquals(new BigInteger("30000"), resultSet.getObject(6, BigInteger.class)); + } + + @Test + void shouldGetFloat() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(1, resultSet.getObject(1, Float.class)); + assertEquals(1.F, resultSet.getFloat(1)); + + assertEquals(30000000000f, resultSet.getObject(2, Float.class)); + assertEquals(30000000000.F, resultSet.getFloat(2)); + assertEquals(1.23f, resultSet.getObject(3, Float.class)); - assertEquals(new BigDecimal("1.23"), resultSet.getObject(3, BigDecimal.class)); + assertEquals(1.23f, resultSet.getFloat(3)); + + assertEquals(1.23456789012f, resultSet.getObject(4, Float.class)); + assertEquals(1.23456789012f, resultSet.getFloat(4)); + + assertEquals(1231232.123459999990457054844258706536f, resultSet.getObject(5, Float.class), 0.01); + assertEquals(1231232.123459999990457054844258706536f, resultSet.getFloat(5), 0.01); + + assertEquals(30000.F, resultSet.getObject(6, Float.class)); + assertEquals(30000.F, resultSet.getFloat(6)); + } + + @Test + void shouldGetDouble() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(1, resultSet.getObject(1, Double.class)); + assertEquals(1., resultSet.getDouble(1)); + + assertEquals(30000000000., resultSet.getObject(2, Double.class)); + assertEquals(30000000000., resultSet.getDouble(2)); + + assertEquals(1.23, resultSet.getObject(3, Double.class)); + assertEquals(1.23, resultSet.getDouble(3)); + assertEquals(1.23456789012, resultSet.getObject(4, Double.class)); + assertEquals(1.23456789012, (double)resultSet.getObject(4, Map.of("double precision", Double.class)), 0.01); + assertEquals(new BigDecimal("1.23456789012"), resultSet.getBigDecimal(4)); + + assertEquals(1231232.123459999990457054844258706536, resultSet.getObject(5, Double.class), 0.01); + assertEquals(1231232.123459999990457054844258706536, resultSet.getDouble(5), 0.01); + + assertEquals(30000., resultSet.getObject(6, Double.class)); + assertEquals(30000., resultSet.getDouble(6)); + } + + @Test + void shouldGetBigDecimal() throws SQLException { + inputStream = getInputStreamWithNumericTypes(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(new BigDecimal("1"), resultSet.getObject(1, BigDecimal.class)); + assertEquals(new BigDecimal("1"), resultSet.getBigDecimal(1)); + + assertEquals(new BigDecimal("30000000000"), resultSet.getObject(2, BigDecimal.class)); + assertEquals(new BigDecimal("30000000000"), resultSet.getBigDecimal(2)); + + assertEquals(new BigDecimal("1.23"), resultSet.getObject(3, BigDecimal.class)); + assertEquals(new BigDecimal("1.23"), resultSet.getBigDecimal(3)); + assertEquals(new BigDecimal("1.23456789012"), resultSet.getObject(4, BigDecimal.class)); + assertEquals(new BigDecimal("1.23456789012"), resultSet.getBigDecimal(4)); + assertEquals(new BigDecimal("1231232.123459999990457054844258706536"), resultSet.getObject(5, BigDecimal.class)); + assertEquals(new BigDecimal("1231232.123459999990457054844258706536"), resultSet.getBigDecimal(5)); + + assertEquals(new BigDecimal("30000"), resultSet.getObject(6, BigDecimal.class)); + assertEquals(new BigDecimal("30000"), resultSet.getBigDecimal(6)); + } + + private void assertTransformationError(int columnIndex, Class type) { + assertTransformationError(columnIndex, i -> resultSet.getObject(i, type)); + } + + private void assertTransformationError(int columnIndex, CheckedFunction getter) { + FireboltException e = assertThrows(FireboltException.class, () -> getter.apply(columnIndex)); + assertEquals(TYPE_TRANSFORMATION_ERROR, e.getType()); + assertEquals(NumberFormatException.class, e.getCause().getClass()); } @Test void shouldReturnDataWithProvidedTypes() throws SQLException { inputStream = getInputStreamWithNewTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals("text", resultSet.getObject(1, String.class)); assertArrayEquals("text".getBytes(), resultSet.getObject(1, byte[].class)); @@ -814,7 +1280,7 @@ void shouldReturnDataWithProvidedTypes() throws SQLException { @Test void shouldReturnDataAndTypesForNumericTypes() throws SQLException { inputStream = getInputStreamWithNumericTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(1, resultSet.getObject(1)); assertEquals(Types.INTEGER, resultSet.getMetaData().getColumnType(1)); @@ -828,7 +1294,7 @@ void shouldReturnDataAndTypesForNumericTypes() throws SQLException { assertEquals(38, resultSet.getMetaData().getPrecision(5)); assertEquals(30, resultSet.getMetaData().getScale(5)); assertEquals(new BigDecimal("1231232.123459999990457054844258706536"), resultSet.getObject(5)); - assertEquals(80000, resultSet.getObject(6)); + assertEquals(30000, resultSet.getObject(6)); assertEquals(Types.INTEGER, resultSet.getMetaData().getColumnType(6)); assertEquals(30000000000L, resultSet.getObject(7)); assertEquals(Types.BIGINT, resultSet.getMetaData().getColumnType(7)); @@ -843,7 +1309,7 @@ void shouldReturnDataAndTypesForNumericTypes() throws SQLException { @Test void shouldReturnDateTimeObjectsWithProvidedTypes() throws SQLException { inputStream = getInputStreamWithNewTypes(); - resultSet = new FireboltResultSet(inputStream, "any", "any", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(Date.valueOf(LocalDate.of(1, 3, 28)), resultSet.getObject(2, Date.class)); assertEquals(Date.valueOf(LocalDate.of(1860, 3, 4)), resultSet.getObject(3, Date.class)); @@ -866,7 +1332,7 @@ void shouldReturnDateTimeObjectsWithProvidedTypes() throws SQLException { @Test void shouldThrowExceptionWhenConvertingIncompatibleTypes() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); FireboltException exception = assertThrows(FireboltException.class, () -> resultSet.getObject(1, String.class)); assertEquals("conversion to class java.lang.String from java.lang.Integer not supported", exception.getMessage()); @@ -875,7 +1341,7 @@ void shouldThrowExceptionWhenConvertingIncompatibleTypes() throws SQLException { @Test void shouldThrowExceptionWhenConvertingUnsupportedTypes() throws SQLException { inputStream = getInputStreamWithCommonResponseExample(); - resultSet = new FireboltResultSet(inputStream, "any_name", "array_db", 65535); + resultSet = createResultSet(inputStream); resultSet.next(); FireboltException exception = assertThrows(FireboltException.class, () -> resultSet.getObject(1, TimeZone.class)); assertEquals("conversion to java.util.TimeZone from java.lang.Integer not supported", exception.getMessage()); @@ -884,7 +1350,7 @@ void shouldThrowExceptionWhenConvertingUnsupportedTypes() throws SQLException { @Test void shouldConvertInt64s() throws SQLException { inputStream = getInputStreamWithBigInt64(); - resultSet = new FireboltResultSet(inputStream); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(new BigInteger("18446744073709551615"), resultSet.getObject(1, BigInteger.class)); assertEquals(new BigInteger("-9223372036854775807"), resultSet.getObject(2, BigInteger.class)); @@ -893,23 +1359,28 @@ void shouldConvertInt64s() throws SQLException { @Test void shouldThrowIntegerInfinity() throws SQLException { inputStream = getInputStreamWithInfinity(); - resultSet = new FireboltResultSet(inputStream); + resultSet = createResultSet(inputStream); resultSet.next(); - assertThrows(IllegalArgumentException.class, () -> resultSet.getShort(1)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getInt(1)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getLong(1)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getShort(2)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getInt(2)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getLong(2)); + assertIllegalArgumentExceptionCause(() -> resultSet.getShort(1)); + assertIllegalArgumentExceptionCause(() -> resultSet.getInt(1)); + assertIllegalArgumentExceptionCause(() -> resultSet.getLong(1)); + + assertIllegalArgumentExceptionCause(() -> resultSet.getShort(2)); + assertIllegalArgumentExceptionCause(() -> resultSet.getInt(2)); + assertIllegalArgumentExceptionCause(() -> resultSet.getLong(2)); - assertThrows(IllegalArgumentException.class, () -> resultSet.getObject(1, BigInteger.class)); + assertIllegalArgumentExceptionCause(() -> resultSet.getObject(1, BigInteger.class)); + } + + private void assertIllegalArgumentExceptionCause(Executable getter) { + assertEquals(IllegalArgumentException.class, assertThrows(SQLException.class, getter).getCause().getClass()); } @Test void shouldConvertFloatInfinity() throws SQLException { inputStream = getInputStreamWithInfinity(); - resultSet = new FireboltResultSet(inputStream); + resultSet = createResultSet(inputStream); resultSet.next(); assertEquals(Float.POSITIVE_INFINITY, resultSet.getFloat(1)); @@ -924,10 +1395,22 @@ void shouldConvertFloatInfinity() throws SQLException { assertEquals(Float.NEGATIVE_INFINITY, resultSet.getObject(2, Float.class)); } + @Test + void shouldConvertFloatNotANumber() throws SQLException { + inputStream = getInputStreamWithInfinity(); + resultSet = createResultSet(inputStream); + resultSet.next(); + + assertEquals(Float.NaN, resultSet.getFloat(3)); + assertEquals(Double.NaN, resultSet.getDouble(3)); + assertEquals(Float.NaN, resultSet.getFloat(4)); + assertEquals(Double.NaN, resultSet.getDouble(4)); + } + @Test void shouldReadArray() throws SQLException { inputStream = getInputStreamWithArray(); - resultSet = new FireboltResultSet(inputStream); + resultSet = createResultSet(inputStream); assertTrue(resultSet.next()); Object intArray = resultSet.getObject(1); assertEquals(Integer[].class, intArray.getClass()); @@ -935,6 +1418,78 @@ void shouldReadArray() throws SQLException { assertFalse(resultSet.next()); } + @Test + void unwrap() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + for (Class type : new Class[] {ResultSet.class, Wrapper.class, AutoCloseable.class, FireboltResultSet.class}) { + assertTrue(resultSet.isWrapperFor(type)); + assertSame(resultSet, resultSet.unwrap(type)); + } + assertFalse(resultSet.isWrapperFor(Runnable.class)); + assertEquals("Cannot unwrap to " + Runnable.class.getName(), assertThrows(SQLException.class, () -> resultSet.unwrap(Runnable.class)).getMessage()); + } + + @Test + @SuppressWarnings("java:S1874") // getUnicodeStream is deprecated byt must be tested + void shouldReturnStream() throws SQLException, IOException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.next(); + String expectedStr = "Taylor's Prime Steak House"; + byte[] expected = expectedStr.getBytes(); + assertStream(expected, resultSet.getAsciiStream(3)); + assertStream(expected, resultSet.getAsciiStream("name")); + //noinspection deprecation + assertStream(expected, resultSet.getUnicodeStream(3)); + //noinspection deprecation + assertStream(expected, resultSet.getUnicodeStream("name")); + assertEquals(expectedStr, IOUtils.toString(resultSet.getCharacterStream(3))); + assertEquals(expectedStr, IOUtils.toString(resultSet.getCharacterStream("name"))); + assertEquals(expectedStr, IOUtils.toString(resultSet.getNCharacterStream(3))); + assertEquals(expectedStr, IOUtils.toString(resultSet.getNCharacterStream("name"))); + } + + @Test + void shouldReturnUrl() throws SQLException, MalformedURLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.next(); + assertEquals(new URL("http://firebolt.io"), resultSet.getURL(8)); + assertEquals(new URL("http://firebolt.io"), resultSet.getURL("url")); + assertEquals(MalformedURLException.class, assertThrows(SQLException.class, () -> resultSet.getURL(3)).getCause().getClass()); + assertEquals(MalformedURLException.class, assertThrows(SQLException.class, () -> resultSet.getURL("name")).getCause().getClass()); + resultSet.next(); + assertNull(resultSet.getURL(8)); + assertNull(resultSet.getURL("url")); + } + + @Test + void shouldBeCaseInsensitive() throws SQLException { + inputStream = getInputStreamWithCommonResponseExample(); + resultSet = createResultSet(inputStream); + resultSet.next(); + ResultSetMetaData rsmd = resultSet.getMetaData(); + int n = rsmd.getColumnCount(); + for (int i = 1; i <= n; i++) { + String columnName = rsmd.getColumnName(i); + Object value = resultSet.getObject(columnName); + Object upperCaseValue = resultSet.getObject(columnName.toUpperCase()); + Object lowerCaseValue = resultSet.getObject(columnName.toLowerCase()); + if (rsmd.getColumnType(i) == Types.ARRAY) { + assertArrayEquals((Object[])value, (Object[])upperCaseValue); + assertArrayEquals((Object[])value, (Object[])lowerCaseValue); + } else { + assertEquals(value, upperCaseValue); + assertEquals(value, lowerCaseValue); + } + } + } + + private void assertStream(byte[] expected, InputStream stream) throws IOException { + assertArrayEquals(expected, stream == null ? null : stream.readAllBytes()); + } + private InputStream getInputStreamWithCommonResponseExample() { return FireboltResultSetTest.class.getResourceAsStream("/responses/firebolt-response-example"); } @@ -982,4 +1537,8 @@ private InputStream getInputStreamWithNumericTypes() { private InputStream getInputStreamWithArray() { return FireboltResultSetTest.class.getResourceAsStream("/responses/firebolt-response-with-array"); } + + private ResultSet createResultSet(InputStream is) throws SQLException { + return new FireboltResultSet(is, "a_table", "a_db", 65535, false, fireboltStatement, true); + } } diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltAccountIdServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltAccountIdServiceTest.java new file mode 100644 index 00000000..255724f1 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltAccountIdServiceTest.java @@ -0,0 +1,21 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccount; +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import org.junit.jupiter.api.Test; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FireboltAccountIdServiceTest { + @Test + void getValue() throws SQLException { + FireboltAccountRetriever firebolAccountClient = mock(FireboltAccountRetriever.class); + FireboltAccount account = new FireboltAccount("id", "region", 123); + when(firebolAccountClient.retrieve("token", "account")).thenReturn(account); + assertSame(account, new FireboltAccountIdService(firebolAccountClient).getValue("token", "account")); + } +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java index 457ee25b..6c05ae6e 100644 --- a/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java +++ b/src/test/java/com/firebolt/jdbc/service/FireboltAuthenticationServiceTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.sql.SQLException; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; @@ -19,6 +20,7 @@ import com.firebolt.jdbc.connection.FireboltConnectionTokens; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; +import com.firebolt.jdbc.exception.SQLState; @ExtendWith(MockitoExtension.class) class FireboltAuthenticationServiceTest { @@ -26,8 +28,9 @@ class FireboltAuthenticationServiceTest { private static final String USER = "usr"; private static final String PASSWORD = "PA§§WORD"; - private static final FireboltProperties PROPERTIES = FireboltProperties.builder().user(USER).password(PASSWORD) - .compress(true).build(); + private static final String ENV = "ENV"; + + private static final FireboltProperties PROPERTIES = FireboltProperties.builder().principal(USER).secret(PASSWORD).environment(ENV).compress(true).build(); @Mock private FireboltAuthenticationClient fireboltAuthenticationClient; @@ -40,36 +43,32 @@ void setUp() { } @Test - void shouldGetConnectionToken() throws IOException, FireboltException { + void shouldGetConnectionToken() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); - FireboltConnectionTokens tokens = FireboltConnectionTokens.builder().expiresInSeconds(52) - .refreshToken("refresh").accessToken("access").build(); - when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD)).thenReturn(tokens); + FireboltConnectionTokens tokens = new FireboltConnectionTokens("access", 52); + when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)).thenReturn(tokens); assertEquals(tokens, fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); - verify(fireboltAuthenticationClient).postConnectionTokens(randomHost, USER, PASSWORD); + verify(fireboltAuthenticationClient).postConnectionTokens(randomHost, USER, PASSWORD, ENV); } @Test - void shouldCallClientOnlyOnceWhenServiceCalledTwiceForTheSameHost() throws IOException, FireboltException { + void shouldCallClientOnlyOnceWhenServiceCalledTwiceForTheSameHost() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); - FireboltConnectionTokens tokens = FireboltConnectionTokens.builder().expiresInSeconds(52) - .refreshToken("refresh").accessToken("access").build(); - when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD)).thenReturn(tokens); + FireboltConnectionTokens tokens = new FireboltConnectionTokens("access", 52); + when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)).thenReturn(tokens); fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES); assertEquals(tokens, fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); - verify(fireboltAuthenticationClient).postConnectionTokens(randomHost, USER, PASSWORD); + verify(fireboltAuthenticationClient).postConnectionTokens(randomHost, USER, PASSWORD, ENV); } @Test - void shouldGetConnectionTokenAfterRemoving() throws IOException, FireboltException { + void shouldGetConnectionTokenAfterRemoving() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); - FireboltConnectionTokens token1 = FireboltConnectionTokens.builder().expiresInSeconds(52) - .refreshToken("refresh").accessToken("one").build(); - FireboltConnectionTokens token2 = FireboltConnectionTokens.builder().expiresInSeconds(52) - .refreshToken("refresh").accessToken("two").build(); - when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD)).thenReturn(token1, token2); + FireboltConnectionTokens token1 = new FireboltConnectionTokens("one", 52); + FireboltConnectionTokens token2 = new FireboltConnectionTokens("two", 52); + when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)).thenReturn(token1, token2); fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES); assertEquals(token1, fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); @@ -77,13 +76,28 @@ void shouldGetConnectionTokenAfterRemoving() throws IOException, FireboltExcepti fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES); assertEquals(token2, fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); - verify(fireboltAuthenticationClient, Mockito.times(2)).postConnectionTokens(randomHost, USER, PASSWORD); + verify(fireboltAuthenticationClient, Mockito.times(2)).postConnectionTokens(randomHost, USER, PASSWORD, ENV); + } + + @Test + void shouldThrowExceptionWithServerResponseWhenAResponseIsAvailable() throws SQLException, IOException { + String randomHost = UUID.randomUUID().toString(); + Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)) + .thenThrow(new FireboltException("An error happened during authentication", 403, "INVALID PASSWORD", + SQLState.INVALID_AUTHORIZATION_SPECIFICATION)); + + FireboltException ex = assertThrows(FireboltException.class, + () -> fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); + assertEquals( + "Failed to connect to Firebolt with the error from the server: INVALID PASSWORD, see logs for more info.", + ex.getMessage()); + assertEquals(SQLState.INVALID_AUTHORIZATION_SPECIFICATION.getCode(), ex.getSQLState()); } @Test - void shouldThrowExceptionWithServerResponseWhenAResponseIsAvailable() throws IOException, FireboltException { + void shouldThrowExceptionNoSQLStateWithServerResponseWhenAResponseIsAvailable() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); - Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD)) + Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)) .thenThrow(new FireboltException("An error happened during authentication", 403, "INVALID PASSWORD")); FireboltException ex = assertThrows(FireboltException.class, @@ -91,17 +105,19 @@ void shouldThrowExceptionWithServerResponseWhenAResponseIsAvailable() throws IOE assertEquals( "Failed to connect to Firebolt with the error from the server: INVALID PASSWORD, see logs for more info.", ex.getMessage()); + assertEquals(null, ex.getSQLState()); } @Test - void shouldThrowExceptionWithExceptionMessageWhenAResponseIsNotAvailable() throws IOException, FireboltException { + void shouldThrowExceptionWithExceptionMessageWhenAResponseIsNotAvailable() throws SQLException, IOException { String randomHost = UUID.randomUUID().toString(); - Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD)) + Mockito.when(fireboltAuthenticationClient.postConnectionTokens(randomHost, USER, PASSWORD, ENV)) .thenThrow(new NullPointerException("NULL!")); FireboltException ex = assertThrows(FireboltException.class, () -> fireboltAuthenticationService.getConnectionTokens(randomHost, PROPERTIES)); assertEquals("Failed to connect to Firebolt with the error: NULL!, see logs for more info.", ex.getMessage()); + assertEquals(null, ex.getSQLState()); } } diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java new file mode 100644 index 00000000..97b50797 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineApiServiceTest.java @@ -0,0 +1,172 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountClient; +import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; +import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; +import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class FireboltEngineApiServiceTest { + private static final String HOST = "host"; + private static final String URL = "http://host"; + private static final String ACCOUNT_ID = "account_id"; + private static final String DB_NAME = "dbName"; + private static final String ENGINE_NAME = "engineName"; + private static final String ENGINE_ID = "engineId"; + private static final String ACCESS_TOKEN = "token"; + private static final FireboltProperties PROPERTIES_WITHOUT_ENGINE = fireboltProperties(ACCOUNT_ID, null); + private static final FireboltProperties PROPERTIES_WITHOUT_ACCOUNT = fireboltProperties(null, ENGINE_NAME); + private static final FireboltProperties PROPERTIES_WITH_ACCOUNT_AND_ENGINE = fireboltProperties(ACCOUNT_ID, ENGINE_NAME); + + @Mock + private FireboltAccountClient fireboltAccountClient; + + @InjectMocks + private FireboltEngineApiService fireboltEngineService; + + @Test + void shouldGetDefaultDbEngineWhenEngineNameIsNullOrEmpty() throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITHOUT_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITHOUT_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltDefaultDatabaseEngineResponse("URL")); + fireboltEngineService.getEngine(PROPERTIES_WITHOUT_ENGINE); + + verify(fireboltAccountClient).getAccount(URL, PROPERTIES_WITHOUT_ENGINE.getAccount(), ACCESS_TOKEN); + verify(fireboltAccountClient).getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenAccountThrowsException() throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITHOUT_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITHOUT_ENGINE.getAccount(), ACCESS_TOKEN)).thenThrow(new IOException()); + assertEquals(IOException.class, assertThrows(FireboltException.class, () ->fireboltEngineService.getEngine(PROPERTIES_WITHOUT_ENGINE)).getCause().getClass()); + } + + @Test + void shouldGThrowExceptionWhenGettingDefaultEngineAndTheUrlReturnedFromTheServerIsNull() throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITHOUT_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITHOUT_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltDefaultDatabaseEngineResponse(null)); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(PROPERTIES_WITHOUT_ENGINE)); + assertEquals( + "There is no Firebolt engine running on http://host attached to the database dbName. To connect first make sure there is a running engine and then try again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(PROPERTIES_WITHOUT_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITHOUT_ENGINE.getAccount(), ACCESS_TOKEN); + verify(fireboltAccountClient).getDefaultEngineByDatabaseName(URL, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldGetEngineWhenEngineNameIsPresent() throws Exception { + when(fireboltAccountClient.getAccount(URL, PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineIdResponse(new FireboltEngineIdResponse.Engine(ENGINE_ID))); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineResponse(new FireboltEngineResponse.Engine("ANY", null))); + fireboltEngineService.getEngine(PROPERTIES_WITH_ACCOUNT_AND_ENGINE); + + verify(fireboltAccountClient).getAccount(URL, ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldNotGetAccountWhileGettingEngineIfAccountIdIsNotPresent() throws Exception { + when(fireboltAccountClient.getEngineId(URL, null, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineIdResponse(new FireboltEngineIdResponse.Engine(ENGINE_ID))); + when(fireboltAccountClient.getEngine(URL, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineResponse(new FireboltEngineResponse.Engine("ANY", null))); + fireboltEngineService.getEngine(PROPERTIES_WITHOUT_ACCOUNT); + + verify(fireboltAccountClient, times(0)).getAccount(any(), any(), any()); + verify(fireboltAccountClient).getEngineId(URL, null, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenEngineNameIsSpecifiedButUrlIsNotPresentInTheResponse() throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineIdResponse(new FireboltEngineIdResponse.Engine(ENGINE_ID))); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineResponse(new FireboltEngineResponse.Engine(null, null))); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(PROPERTIES_WITH_ACCOUNT_AND_ENGINE)); + assertEquals( + "There is no Firebolt engine running on http://host with the name engineName. To connect first make sure there is a running engine and then try again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @Test + void shouldThrowExceptionWhenEngineNameIsSpecifiedButEngineIdIsNotPresentInTheServerResponse() throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineIdResponse(new FireboltEngineIdResponse.Engine(null))); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(PROPERTIES_WITH_ACCOUNT_AND_ENGINE)); + assertEquals( + "Failed to extract engine id field from the server response: the response from the server is invalid.", + exception.getMessage()); + verify(fireboltAccountClient).getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + @ParameterizedTest + @ValueSource(strings = { "ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_FINISHED", "ENGINE_STATUS_PROVISIONING_PENDING" }) + void shouldThrowExceptionWhenEngineStatusIndicatesEngineIsStarting(String status) throws Exception { + when(fireboltAccountClient.getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getAccount(), ACCESS_TOKEN)) + .thenReturn(new FireboltAccountResponse(ACCOUNT_ID)); + when(fireboltAccountClient.getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineIdResponse(new FireboltEngineIdResponse.Engine(ENGINE_ID))); + when(fireboltAccountClient.getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) + .thenReturn(new FireboltEngineResponse(new FireboltEngineResponse.Engine("ANY", status))); + FireboltException exception = assertThrows(FireboltException.class, + () -> fireboltEngineService.getEngine(PROPERTIES_WITH_ACCOUNT_AND_ENGINE)); + assertEquals("The engine engineName is currently starting. Please wait until the engine is on and then execute the query again.", + exception.getMessage()); + + verify(fireboltAccountClient).getAccount(PROPERTIES_WITH_ACCOUNT_AND_ENGINE.getHttpConnectionUrl(), ACCOUNT_ID, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngineId(URL, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); + verify(fireboltAccountClient).getEngine(URL, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); + verifyNoMoreInteractions(fireboltAccountClient); + } + + private static FireboltProperties fireboltProperties(String account, String engine) { + return FireboltProperties.builder().host(HOST).account(account).database(DB_NAME).engine(engine).compress(false).accessToken(ACCESS_TOKEN).build(); + } +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java new file mode 100644 index 00000000..d4ab80fb --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceTest.java @@ -0,0 +1,130 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.exception.FireboltException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.regex.Pattern; + +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +abstract class FireboltEngineInformationSchemaServiceTest { + + protected FireboltEngineInformationSchemaService fireboltEngineService; + + @Mock + protected FireboltConnection fireboltConnection; + private final boolean useCatalogTable; + + FireboltEngineInformationSchemaServiceTest(boolean useCatalogTable) { + this.useCatalogTable = useCatalogTable; + } + + @BeforeEach + void init() throws SQLException { + PreparedStatement catalogsStatement = mock(PreparedStatement.class); + Map catalogsRsData = useCatalogTable ? Map.of("table_name", "catalogs") : Map.of(); + ResultSet catalogsResultSet = mockedResultSet(catalogsRsData); + when(fireboltConnection.prepareStatement("SELECT table_name FROM information_schema.tables WHERE table_name=?")).thenReturn(catalogsStatement); + when(catalogsStatement.executeQuery()).thenReturn(catalogsResultSet); + fireboltEngineService = new FireboltEngineInformationSchemaService(fireboltConnection); + } + + @Test + void shouldThrowExceptionEngineWhenEngineNameIsNotProvided() { + FireboltProperties properties = FireboltProperties.builder().database("db").build(); + assertThrows(IllegalArgumentException.class, () -> fireboltEngineService.getEngine(properties)); + } + + @ParameterizedTest + @CsvSource({ + /*in:*/ "db,running,my-url,my-url," + /*expected:*/ "some-engine,db,", + /*in:*/ ",running,api.region.env.firebolt.io,api.region.env.firebolt.io," + /*expected:*/ "some-engine,,", + /*in:*/ "db,ENGINE_STATE_RUNNING,api.us-east-1.dev.firebolt.io?account_id=01hf9pchg0mnrd2g3hypm1dea4&engine=max_test,api.us-east-1.dev.firebolt.io," + /*expected:*/ "max_test,db,01hf9pchg0mnrd2g3hypm1dea4", + }) + void shouldGetEngineWhenEngineNameIsProvided(String db, String engineStatus, String engineUrl, String expectedEngineUrl, String expectedEngine, String expectedDb, String expectedAccountId) throws SQLException { + PreparedStatement statement = mock(PreparedStatement.class); + ResultSet resultSet = mockedResultSet(Map.of("status", engineStatus, "url", engineUrl, "attached_to", "db", "engine_name", "some-engine")); + when(fireboltConnection.prepareStatement(anyString())).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + FireboltProperties properties = createFireboltProperties("some-engine", db); + assertEquals(new Engine(expectedEngineUrl, engineStatus, "some-engine", "db", null), fireboltEngineService.getEngine(properties)); + assertEquals(expectedEngine, properties.getEngine()); + assertEquals(expectedDb, properties.getDatabase()); + assertEquals(expectedAccountId, properties.getAccountId()); + } + + @ParameterizedTest + @CsvSource(value = { + "engine1;db1;http://url1;running;;The engine with the name engine1 is not attached to any database", + "engine1;db1;http://url1;running;db2;The engine with the name engine1 is not attached to database db1", + "engine1;db1;http://url1;starting;;The engine with the name engine1 is not running. Status: starting", + "engine2;;;;;The engine with the name engine2 could not be found", + }, delimiter = ';') + void shouldThrowExceptionWhenSomethingIsWrong(String engineName, String db, String endpoint, String status, String attachedDb, String errorMessage) throws SQLException { + PreparedStatement statement = mock(PreparedStatement.class); + Map rsData = null; + if (endpoint != null || status != null || attachedDb != null) { + rsData = new HashMap<>(); + rsData.put("url", endpoint); + rsData.put("status", status); + rsData.put("attached_to", attachedDb); + rsData.put("engine_name", engineName); + } + ResultSet resultSet = mockedResultSet(rsData); + when(fireboltConnection.prepareStatement(Mockito.matches(Pattern.compile("SELECT.+JOIN", Pattern.MULTILINE | Pattern.DOTALL)))).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + assertEquals(errorMessage, assertThrows(FireboltException.class, () -> fireboltEngineService.getEngine(createFireboltProperties(engineName, db))).getMessage()); + Mockito.verify(statement, Mockito.times(1)).setString(1, engineName); + } + + private PreparedStatement mockedEntityStatement(String entity, String row) throws SQLException { + if (row == null) { + return null; + } + PreparedStatement statement = mock(PreparedStatement.class); + ResultSet resultSet = mockedResultSet(row.isEmpty() ? Map.of() : Map.of(row.split(",")[0], row.split(",")[1])); + when(fireboltConnection.prepareStatement(format("SELECT * FROM information_schema.%ss WHERE %s_name=?", entity, entity))).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + return statement; + } + + protected ResultSet mockedResultSet(Map values) throws SQLException { + ResultSet resultSet = mock(ResultSet.class); + if (values == null || values.isEmpty()) { + when(resultSet.next()).thenReturn(false); + } else { + when(resultSet.next()).thenReturn(true); + for (Entry column : values.entrySet()) { + lenient().when(resultSet.getString(column.getKey())).thenReturn(column.getValue()); + } + } + return resultSet; + } + + private FireboltProperties createFireboltProperties(String engine, String database) { + return FireboltProperties.builder().engine(engine).database(database).build(); + } +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingCatalogsTableTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingCatalogsTableTest.java new file mode 100644 index 00000000..bd867040 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingCatalogsTableTest.java @@ -0,0 +1,33 @@ +package com.firebolt.jdbc.service; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mockito; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FireboltEngineInformationSchemaServiceUsingCatalogsTableTest extends FireboltEngineInformationSchemaServiceTest{ + FireboltEngineInformationSchemaServiceUsingCatalogsTableTest() { + super(true); + } + + @ParameterizedTest + @CsvSource(value = {"mydb;'';false", "other_db;'database_name,other_db';true"}, delimiter = ';') + void doesDatabaseExist(String db, String row, boolean expected) throws SQLException { + PreparedStatement statement = mock(PreparedStatement.class); + Map rowData = row == null || row.isEmpty() ? Map.of() : Map.of(row.split(",")[0], row.split(",")[1]); + ResultSet resultSet = mockedResultSet(rowData); + when(fireboltConnection.prepareStatement("SELECT catalog_name FROM information_schema.catalogs WHERE catalog_name=?")).thenReturn(statement); + when(statement.executeQuery()).thenReturn(resultSet); + assertEquals(expected, fireboltEngineService.doesDatabaseExist(db)); + Mockito.verify(statement, Mockito.times(1)).setString(1, db); + } + +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingDatabasesTableTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingDatabasesTableTest.java new file mode 100644 index 00000000..aebc6412 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineInformationSchemaServiceUsingDatabasesTableTest.java @@ -0,0 +1,7 @@ +package com.firebolt.jdbc.service; + +class FireboltEngineInformationSchemaServiceUsingDatabasesTableTest extends FireboltEngineInformationSchemaServiceTest{ + FireboltEngineInformationSchemaServiceUsingDatabasesTableTest() { + super(false); + } +} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java deleted file mode 100644 index ec83e03d..00000000 --- a/src/test/java/com/firebolt/jdbc/service/FireboltEngineServiceTest.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.firebolt.jdbc.service; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; - -import java.sql.SQLException; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.firebolt.jdbc.client.account.FireboltAccountClient; -import com.firebolt.jdbc.client.account.response.FireboltAccountResponse; -import com.firebolt.jdbc.client.account.response.FireboltDefaultDatabaseEngineResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineIdResponse; -import com.firebolt.jdbc.client.account.response.FireboltEngineResponse; -import com.firebolt.jdbc.connection.settings.FireboltProperties; -import com.firebolt.jdbc.exception.FireboltException; - -@ExtendWith(MockitoExtension.class) -class FireboltEngineServiceTest { - - private static final String HOST = "https://host"; - private static final String ACCOUNT_ID = "account_id"; - private static final String DB_NAME = "dbName"; - private static final String ENGINE_NAME = "engineName"; - private static final String ENGINE_ID = "engineId"; - private static final String ACCESS_TOKEN = "token"; - - @Mock - private FireboltAccountClient fireboltAccountClient; - - @InjectMocks - private FireboltEngineService fireboltEngineService; - - @Test - void shouldGetDefaultDbEngineWhenEngineNameIsNullOrEmpty() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .compress(false).build(); - - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getDefaultEngineByDatabaseName(HOST, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltDefaultDatabaseEngineResponse.builder().engineUrl("URL").build()); - fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN); - - verify(fireboltAccountClient).getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN); - verify(fireboltAccountClient).getDefaultEngineByDatabaseName(HOST, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldGThrowExceptionWhenGettingDefaultEngineAndTheUrlReturnedFromTheServerIsNull() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .compress(false).build(); - - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getDefaultEngineByDatabaseName(HOST, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltDefaultDatabaseEngineResponse.builder().engineUrl(null).build()); - FireboltException exception = assertThrows(FireboltException.class, - () -> fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN)); - assertEquals( - "There is no Firebolt engine running on https://host attached to the database dbName. To connect first make sure there is a running engine and then try again.", - exception.getMessage()); - - verify(fireboltAccountClient).getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN); - verify(fireboltAccountClient).getDefaultEngineByDatabaseName(HOST, ACCOUNT_ID, DB_NAME, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldGetEngineWhenEngineNameIsPresent() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .engine(ENGINE_NAME).compress(false).build(); - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); - when(fireboltAccountClient.getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) - .thenReturn(FireboltEngineResponse.builder() - .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").build()).build()); - fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN); - - verify(fireboltAccountClient).getAccount(properties.getHost(), ACCOUNT_ID, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldNotGetAccountWhileGettingEngineIfAccountIdIsNotPresent() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).database(DB_NAME) - .engine(ENGINE_NAME).compress(false).build(); - when(fireboltAccountClient.getEngineId(HOST, null, ENGINE_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); - when(fireboltAccountClient.getEngine(HOST, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) - .thenReturn(FireboltEngineResponse.builder() - .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").build()).build()); - fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN); - - verify(fireboltAccountClient, times(0)).getAccount(any(), any(), any()); - verify(fireboltAccountClient).getEngineId(HOST, null, ENGINE_NAME, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngine(HOST, null, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldThrowExceptionWhenEngineNameIsSpecifiedButUrlIsNotPresentInTheResponse() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .engine(ENGINE_NAME).compress(false).build(); - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); - when(fireboltAccountClient.getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) - .thenReturn(FireboltEngineResponse.builder() - .engine(FireboltEngineResponse.Engine.builder().endpoint(null).build()).build()); - FireboltException exception = assertThrows(FireboltException.class, - () -> fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN)); - assertEquals( - "There is no Firebolt engine running on https://host with the name engineName. To connect first make sure there is a running engine and then try again.", - exception.getMessage()); - - verify(fireboltAccountClient).getAccount(properties.getHost(), ACCOUNT_ID, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldThrowExceptionWhenEngineNameIsSpecifiedButEngineIdIsNotPresentInTheServerResponse() throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .engine(ENGINE_NAME).compress(false).build(); - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId(null).build()).build()); - FireboltException exception = assertThrows(FireboltException.class, - () -> fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN)); - assertEquals( - "Failed to extract engine id field from the server response: the response from the server is invalid.", - exception.getMessage()); - verify(fireboltAccountClient).getAccount(properties.getHost(), ACCOUNT_ID, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } - - @Test - void shouldGetEngineNameFromEngineHost() throws SQLException { - assertEquals("myHost_345", fireboltEngineService.getEngineNameFromHost("myHost-345.firebolt.io")); - } - - @Test - void shouldThrowExceptionWhenThEngineCannotBeEstablishedFromTheHost() { - assertThrows(FireboltException.class, () -> fireboltEngineService.getEngineNameFromHost("myHost-345")); - } - - @Test - void shouldThrowExceptionWhenThEngineCannotBeEstablishedFromNullHost() { - assertThrows(FireboltException.class, () -> fireboltEngineService.getEngineNameFromHost(null)); - } - - @ParameterizedTest - @ValueSource(strings = { "ENGINE_STATUS_PROVISIONING_STARTED", "ENGINE_STATUS_PROVISIONING_FINISHED", - "ENGINE_STATUS_PROVISIONING_PENDING" }) - void shouldThrowExceptionWhenEngineStatusIndicatesEngineIsStarting(String status) throws Exception { - FireboltProperties properties = FireboltProperties.builder().host(HOST).account(ACCOUNT_ID).database(DB_NAME) - .engine(ENGINE_NAME).compress(false).build(); - when(fireboltAccountClient.getAccount(properties.getHost(), properties.getAccount(), ACCESS_TOKEN)) - .thenReturn(FireboltAccountResponse.builder().accountId(ACCOUNT_ID).build()); - when(fireboltAccountClient.getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN)) - .thenReturn(FireboltEngineIdResponse.builder() - .engine(FireboltEngineIdResponse.Engine.builder().engineId(ENGINE_ID).build()).build()); - when(fireboltAccountClient.getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN)) - .thenReturn(FireboltEngineResponse.builder() - .engine(FireboltEngineResponse.Engine.builder().endpoint("ANY").currentStatus(status).build()) - .build()); - FireboltException exception = assertThrows(FireboltException.class, - () -> fireboltEngineService.getEngine(HOST, properties, ACCESS_TOKEN)); - assertEquals("The engine engineName is currently starting. Please wait until the engine is on and then execute the query again.", - exception.getMessage()); - - verify(fireboltAccountClient).getAccount(properties.getHost(), ACCOUNT_ID, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngineId(HOST, ACCOUNT_ID, ENGINE_NAME, ACCESS_TOKEN); - verify(fireboltAccountClient).getEngine(HOST, ACCOUNT_ID, ENGINE_NAME, ENGINE_ID, ACCESS_TOKEN); - verifyNoMoreInteractions(fireboltAccountClient); - } -} diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltEngineVersion2ServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltEngineVersion2ServiceTest.java new file mode 100644 index 00000000..a7c9889a --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltEngineVersion2ServiceTest.java @@ -0,0 +1,49 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.connection.Engine; +import com.firebolt.jdbc.connection.FireboltConnection; +import com.firebolt.jdbc.connection.settings.FireboltProperties; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class FireboltEngineVersion2ServiceTest { + @ParameterizedTest(name = "database={0}") + @ValueSource(strings = {"my_database"}) + @NullSource + void getEndine(String database) throws SQLException { + String engine = "my_engine"; + Properties props = new Properties(); + if (database != null) { + props.setProperty("database", database); + } + props.setProperty("engine", engine); + String endpoint = "api.region.firebolt.io"; + FireboltProperties properties = new FireboltProperties(props); + FireboltConnection connection = mock(FireboltConnection.class); + Statement statement = mock(Statement.class); + when(connection.createStatement()).thenReturn(statement); + if (database != null) { + when(statement.executeUpdate("USE DATABASE " + database)).thenReturn(1); + } else { + verify(statement, never()).executeQuery("USE DATABASE " + database); + } + when(statement.executeUpdate("USE ENGINE " + engine)).thenReturn(1); + when(connection.getSessionProperties()).thenReturn(properties); + when(connection.getEndpoint()).thenReturn(endpoint); + + FireboltEngineVersion2Service service = new FireboltEngineVersion2Service(connection); + Engine actualEngine = service.getEngine(properties); + assertEquals(new Engine(endpoint, null, engine, database, null), actualEngine); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltGatewayUrlServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltGatewayUrlServiceTest.java new file mode 100644 index 00000000..6e840116 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/service/FireboltGatewayUrlServiceTest.java @@ -0,0 +1,33 @@ +package com.firebolt.jdbc.service; + +import com.firebolt.jdbc.client.account.FireboltAccountRetriever; +import com.firebolt.jdbc.client.gateway.GatewayUrlResponse; +import com.firebolt.jdbc.exception.FireboltException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class FireboltGatewayUrlServiceTest { + @ParameterizedTest + @CsvSource({ + "http://host,http://host", + "https://host,https://host", + "host,https://host", // force https + "http://host?name=value,http://host?name=value", + "https://host?name=value,https://host?name=value", + "host?name=value,https://host?name=value", + }) + void test(String rawUrl, String expectedUrl) throws SQLException { + @SuppressWarnings("unchecked") + FireboltAccountRetriever fireboltGatewayUrlClient = mock(FireboltAccountRetriever.class); + when(fireboltGatewayUrlClient.retrieve("token", "account")).thenReturn(new GatewayUrlResponse(rawUrl)); + String actualUrl = new FireboltGatewayUrlService(fireboltGatewayUrlClient).getUrl("token", "account"); + assertEquals(expectedUrl, actualUrl); + } + +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java b/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java index 01b41efd..61d2f584 100644 --- a/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java +++ b/src/test/java/com/firebolt/jdbc/service/FireboltStatementServiceTest.java @@ -1,29 +1,37 @@ package com.firebolt.jdbc.service; -import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.*; - -import java.sql.SQLException; -import java.util.Optional; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.MockedConstruction; -import org.mockito.Mockito; -import org.mockito.junit.jupiter.MockitoExtension; - import com.firebolt.jdbc.client.query.StatementClient; +import com.firebolt.jdbc.client.query.StatementClientImpl; +import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.statement.FireboltStatement; import com.firebolt.jdbc.statement.StatementInfoWrapper; import com.firebolt.jdbc.statement.StatementUtil; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.sql.SQLException; + +import static java.util.Optional.empty; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FireboltStatementServiceTest { + private final FireboltProperties emptyFireboltProperties = FireboltProperties.builder().build(); @Mock private StatementClient statementClient; @@ -33,14 +41,13 @@ void shouldExecuteQueryAndCreateResultSet() throws SQLException { try (MockedConstruction mocked = Mockito.mockConstruction(FireboltResultSet.class)) { StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("SELECT 1").get(0); - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("firebolt1") - .ssl(true).compress(false).build(); - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, false); - - fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, 10, -1, true, - mock(FireboltStatement.class)); - verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, 10, true); - Assertions.assertEquals(1, mocked.constructed().size()); + FireboltProperties fireboltProperties = fireboltProperties("firebolt1", false); + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + FireboltStatement statement = mock(FireboltStatement.class); + when(statement.getQueryTimeout()).thenReturn(10); + fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, statement); + verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, 10); + assertEquals(1, mocked.constructed().size()); } } @@ -48,32 +55,29 @@ void shouldExecuteQueryAndCreateResultSet() throws SQLException { void shouldExecuteQueryWithLocalHostFormatParameters() throws SQLException { try (MockedConstruction mocked = Mockito.mockConstruction(FireboltResultSet.class)) { StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("SELECT 1").get(0); - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("localhost") - .ssl(true).compress(false).build(); - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, false); - fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, -1, 10, true, - mock(FireboltStatement.class)); - Assertions.assertEquals(1, mocked.constructed().size()); - verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, -1, true); + FireboltProperties fireboltProperties = fireboltProperties("localhost", false); + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + FireboltStatement statement = mock(FireboltStatement.class); + when(statement.getQueryTimeout()).thenReturn(-1); + fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, statement); + assertEquals(1, mocked.constructed().size()); + verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, -1); } } @Test - void shouldCancelQueryWithAllRequiredParams() throws FireboltException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("http://firebolt1") - .ssl(true).compress(false).build(); - - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, false); + void shouldCancelQueryWithAllRequiredParams() throws SQLException { + FireboltProperties fireboltProperties = fireboltProperties("firebolt1", false); + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); fireboltStatementService.abortStatement("123", fireboltProperties); verify(statementClient).abortStatement("123", fireboltProperties); } @Test - void shouldThrowExceptionWhenTryingToCancelQueryWithASystemEngine() throws FireboltException { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("http://firebolt1") - .ssl(true).compress(false).build(); - - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, true); + void shouldThrowExceptionWhenTryingToCancelQueryWithASystemEngine() { + FireboltProperties fireboltProperties = fireboltProperties("firebolt1", true); + StatementClient client = new StatementClientImpl(new OkHttpClient(), Mockito.mock(FireboltConnection.class), null, null); + FireboltStatementService fireboltStatementService = new FireboltStatementService(client); assertThrows(FireboltException.class, () -> fireboltStatementService.abortStatement("123", fireboltProperties)); verifyNoInteractions(statementClient); } @@ -82,43 +86,45 @@ void shouldThrowExceptionWhenTryingToCancelQueryWithASystemEngine() throws Fireb void shouldExecuteQueryWithParametersForSystemEngine() throws SQLException { try (MockedConstruction mocked = Mockito.mockConstruction(FireboltResultSet.class)) { StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("SELECT 1").get(0); - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("firebolt1") - .ssl(true).compress(false).build(); - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, true); - fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, 10, 10, true, - mock(FireboltStatement.class)); - Assertions.assertEquals(1, mocked.constructed().size()); - verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, true, 10, true); + FireboltProperties fireboltProperties = fireboltProperties("firebolt1", true); + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + FireboltStatement statement = mock(FireboltStatement.class); + when(statement.getQueryTimeout()).thenReturn(10); + fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, statement); + assertEquals(1, mocked.constructed().size()); + verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, true, 10); } } @Test - void shouldIncludeNonStandardSqlQueryParamForNonStandardSql() throws SQLException { - try (MockedConstruction mocked = Mockito.mockConstruction(FireboltResultSet.class)) { + void shouldBeEmptyWithNonQueryStatement() throws SQLException { + StatementInfoWrapper statementInfoWrapper = StatementUtil + .parseToStatementInfoWrappers("INSERT INTO ltv SELECT * FROM ltv_external").get(0); + FireboltProperties fireboltProperties = fireboltProperties("localhost", false); - StatementInfoWrapper statementInfoWrapper = StatementUtil.parseToStatementInfoWrappers("SELECT 1").get(0); - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("localhost") - .ssl(true).compress(false).build(); - - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, true); - fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, -1, 0, false, - mock(FireboltStatement.class)); - Assertions.assertEquals(1, mocked.constructed().size()); - verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, true, -1, false); - } + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + FireboltStatement statement = mock(FireboltStatement.class); + when(statement.getQueryTimeout()).thenReturn(-1); + assertEquals(empty(), fireboltStatementService.execute(statementInfoWrapper, fireboltProperties, statement)); + verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, -1); } @Test - void shouldBeEmptyWithNonQueryStatement() throws SQLException { - StatementInfoWrapper statementInfoWrapper = StatementUtil - .parseToStatementInfoWrappers("INSERT INTO ltv SELECT * FROM ltv_external").get(0); - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db").host("localhost").ssl(true) - .compress(false).build(); + void abortStatementHttpRequest() throws SQLException { + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + fireboltStatementService.abortStatement("id", emptyFireboltProperties); + verify(statementClient).abortStatement("id", emptyFireboltProperties); + } - FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient, false); - Assertions.assertEquals(Optional.empty(), fireboltStatementService.execute(statementInfoWrapper, - fireboltProperties, -1, 10, true, mock(FireboltStatement.class))); - verify(statementClient).executeSqlStatement(statementInfoWrapper, fireboltProperties, false, -1, true); + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void isStatementRunning(boolean running) { + FireboltStatementService fireboltStatementService = new FireboltStatementService(statementClient); + when(statementClient.isStatementRunning("id")).thenReturn(running); + assertEquals(running, fireboltStatementService.isStatementRunning("id")); } + private FireboltProperties fireboltProperties(String host, boolean systemEngine) { + return FireboltProperties.builder().database("db").host(host).ssl(true).compress(false).systemEngine(systemEngine).build(); + } } diff --git a/src/test/java/com/firebolt/jdbc/statement/FireboltStatementTest.java b/src/test/java/com/firebolt/jdbc/statement/FireboltStatementTest.java index 00f99072..0c8d366e 100644 --- a/src/test/java/com/firebolt/jdbc/statement/FireboltStatementTest.java +++ b/src/test/java/com/firebolt/jdbc/statement/FireboltStatementTest.java @@ -1,17 +1,21 @@ package com.firebolt.jdbc.statement; +import com.firebolt.jdbc.CheckedBiFunction; +import com.firebolt.jdbc.client.query.StatementClient; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; +import com.firebolt.jdbc.connection.settings.FireboltSessionProperty; import com.firebolt.jdbc.exception.FireboltException; import com.firebolt.jdbc.resultset.FireboltResultSet; import com.firebolt.jdbc.service.FireboltStatementService; -import lombok.SneakyThrows; -import org.apache.commons.lang3.tuple.ImmutablePair; +import com.firebolt.jdbc.statement.rawstatement.QueryRawStatement; +import com.firebolt.jdbc.type.array.SqlArrayUtil; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; @@ -19,334 +23,559 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.ByteArrayInputStream; import java.io.Closeable; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; +import java.sql.SQLWarning; import java.sql.Statement; import java.sql.Wrapper; +import java.text.MessageFormat; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; import java.util.stream.Stream; +import static java.lang.String.format; import static java.sql.Statement.CLOSE_CURRENT_RESULT; +import static java.sql.Statement.SUCCESS_NO_INFO; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.times; @ExtendWith(MockitoExtension.class) class FireboltStatementTest { - - @Captor - ArgumentCaptor queryInfoWrapperArgumentCaptor; - @Mock - private FireboltStatementService fireboltStatementService; - @Mock - private FireboltConnection fireboltConnection; - - private static FireboltStatement statement; - - private static Stream unsupported() { - return Stream.of( - Arguments.of("clearWarnings", (Executable) () -> statement.clearWarnings()), - Arguments.of("setCursorName", (Executable) () -> statement.setCursorName("my_cursor")), - Arguments.of("addBatch", (Executable) () -> statement.addBatch("insert into people (id, name) values (?, ?))")), - Arguments.of("clearBatch", (Executable) () -> statement.clearBatch()), - Arguments.of("executeBatch", (Executable) () -> statement.executeBatch()), - Arguments.of("getGeneratedKeys", (Executable) () -> statement.getGeneratedKeys()), - Arguments.of("executeUpdate(auto generated keys)", (Executable) () -> statement.executeUpdate("insert", Statement.NO_GENERATED_KEYS)), - Arguments.of("executeUpdate(column indexes)", (Executable) () -> statement.executeUpdate("insert", new int[0])), - Arguments.of("executeUpdate(column names)", (Executable) () -> statement.executeUpdate("insert", new String[0])), - Arguments.of("execute(auto generated keys)", (Executable) () -> statement.execute("insert", Statement.NO_GENERATED_KEYS)), - Arguments.of("execute(column indexes)", (Executable) () -> statement.execute("insert", new int[0])), - Arguments.of("execute(column names)", (Executable) () -> statement.execute("insert", new String[0])), - Arguments.of("setPoolable(true)", (Executable) () -> statement.setPoolable(true)) - ); - } - - @ParameterizedTest(name = "{0}") - @MethodSource("unsupported") - void shouldThrowSQLFeatureNotSupportedException(String name, Executable function) { - statement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(mock(FireboltProperties.class)).connection(mock(FireboltConnection.class)).build(); - assertThrows(SQLFeatureNotSupportedException.class, function); - } - - @Test - void shouldExtractAdditionalPropertiesAndNotExecuteQueryWhenSetParamIsUsed() throws SQLException { - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - - fireboltStatement.execute("set custom_1 = 1"); - verifyNoMoreInteractions(fireboltStatementService); - verify(connection).addProperty(new ImmutablePair<>("custom_1", "1")); - } - - @SneakyThrows - @Test - void shouldAbortStatementOnCancel() { - FireboltProperties fireboltProperties = FireboltProperties.builder().database("db") - .additionalProperties(new HashMap<>()).build(); - - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(fireboltConnection).build(); - - Field runningStatementField = FireboltStatement.class.getDeclaredField("runningStatementId"); - runningStatementField.setAccessible(true); - runningStatementField.set(fireboltStatement, "1234"); - fireboltStatement.cancel(); - verify(fireboltStatementService).abortStatement(any(), eq(fireboltProperties)); - } - - @Test - void shouldCloseInputStreamOnClose() throws SQLException { - ResultSet rs = mock(ResultSet.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltConnection connection = mock(FireboltConnection.class); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.empty()); - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.of(rs)); - fireboltStatement.executeQuery("show database"); - fireboltStatement.close(); - verify(rs).close(); - verify(connection).removeClosedStatement(fireboltStatement); - - // validate that recurrent close does not create cascading closing calls. - fireboltStatement.close(); - verifyNoMoreInteractions(connection); - } - - @Test - void shouldThrowAnExceptionWhenExecutingQueryOnANonQueryStatement() { - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - - assertThrows(FireboltException.class, () -> fireboltStatement.executeQuery("set custom_1 = 1")); - } - - @Test - void shouldExecuteIfUpdateStatementWouldNotReturnAResultSet() throws SQLException { - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - - try (FireboltStatement fireboltStatement = FireboltStatement.builder() - .statementService(fireboltStatementService).connection(fireboltConnection) - .sessionProperties(fireboltProperties).build()) { - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.empty()); - assertEquals(0, fireboltStatement.executeUpdate("INSERT INTO cars(sales, name) VALUES (500, 'Ford')")); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(fireboltProperties), - anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("INSERT INTO cars(sales, name) VALUES (500, 'Ford')", - queryInfoWrapperArgumentCaptor.getValue().getSql()); - assertEquals(0, fireboltStatement.getUpdateCount()); - } - - } - - @Test - void shouldCloseCurrentAndGetMoreResultsForMultiStatementQuery() throws SQLException { - ResultSet rs = mock(FireboltResultSet.class); - ResultSet rs2 = mock(FireboltResultSet.class); - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.of(rs)).thenReturn(Optional.of(rs2)); - fireboltStatement.execute("SELECT 1; SELECT 2;"); - assertEquals(rs, fireboltStatement.getResultSet()); - assertEquals(-1, fireboltStatement.getUpdateCount()); - fireboltStatement.getMoreResults(); - verify(rs).close(); - assertEquals(rs2, fireboltStatement.getResultSet()); - rs = fireboltStatement.getResultSet(); - fireboltStatement.getMoreResults(); - verify(rs).close(); - assertEquals(-1, fireboltStatement.getUpdateCount()); - assertNull(fireboltStatement.getResultSet()); - } - - @Test - void shouldCloseCurrentAndGetMoreResultWhenCallingGetMoreResultsWithCloseCurrentFlag() throws SQLException { - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.of(mock(FireboltResultSet.class))); - fireboltStatement.execute("SELECT 1; SELECT 2;"); - ResultSet resultSet = fireboltStatement.getResultSet(); - fireboltStatement.getMoreResults(CLOSE_CURRENT_RESULT); - verify(resultSet).close(); - } - - @Test - void shouldKeepCurrentAndGetMoreResultWhenCallingGetMoreResultsWithKeepCurrentResultFlag() throws SQLException { - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.of(mock(ResultSet.class))); - - fireboltStatement.execute("SELECT 1; SELECT 2;"); - ResultSet resultSet = fireboltStatement.getResultSet(); - fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); - verify(resultSet, never()).close(); - } - - @Test - void shouldCloseUnclosedAndGetMoreResultWhenCallingGetMoreResultsWithCloseAllResultFlag() throws SQLException { - ResultSet rs = mock(FireboltResultSet.class); - ResultSet rs2 = mock(FireboltResultSet.class); - ResultSet rs3 = mock(FireboltResultSet.class); - FireboltConnection connection = mock(FireboltConnection.class); - FireboltProperties fireboltProperties = FireboltProperties.builder().additionalProperties(new HashMap<>()) - .build(); - FireboltStatement fireboltStatement = FireboltStatement.builder().statementService(fireboltStatementService) - .sessionProperties(fireboltProperties).connection(connection).build(); - - when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.of(rs)).thenReturn(Optional.of(rs2)).thenReturn(Optional.of(rs3)); - - fireboltStatement.execute("SELECT 1; SELECT 2; SELECT 3;"); - ResultSet firstRs = fireboltStatement.getResultSet(); - fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); - verify(firstRs, never()).close(); - ResultSet secondRs = fireboltStatement.getResultSet(); - fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); - verify(secondRs, never()).close(); - fireboltStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS); - verify(firstRs).close(); - verify(secondRs).close(); - - } - - @Test - void maxRows() throws SQLException { - try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { - assertEquals(0, statement.getMaxRows()); // zero means there is no limit - statement.setMaxRows(123); - assertEquals(123, statement.getMaxRows()); - } - } - - @Test - void negativeMaxRows() throws SQLException { - try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { - assertThrows(SQLException.class, () -> statement.setMaxRows(-1)); - } - } - - @Test - void statementParameters() throws SQLException { - try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { - assertEquals(ResultSet.FETCH_FORWARD, statement.getFetchDirection()); - assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); - assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); - assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); - assertNull(statement.getWarnings()); - } - } - - @ParameterizedTest - @ValueSource(classes = {Statement.class, FireboltStatement.class, Wrapper.class, AutoCloseable.class}) - void successfulUnwrap(Class clazz) throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertSame(statement, statement.unwrap(clazz)); - } - - @ParameterizedTest - @ValueSource(classes = {Connection.class, String.class, Closeable.class}) - void failingUnwrap(Class clazz) { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertThrows(SQLException.class, () -> statement.unwrap(clazz)); - } - - @ParameterizedTest - @ValueSource(classes = {Statement.class, FireboltStatement.class, Wrapper.class, AutoCloseable.class}) - void isWrapperFor(Class clazz) throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertTrue(statement.isWrapperFor(clazz)); - } - - @ParameterizedTest - @ValueSource(classes = {Connection.class, String.class, Closeable.class}) - void isNotWrapperFor(Class clazz) throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertFalse(statement.isWrapperFor(clazz)); - } - - @Test - void queryTimeout() throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertEquals(0, statement.getQueryTimeout()); - statement.setQueryTimeout(12345); - assertEquals(12345, statement.getQueryTimeout()); - } - - @Test - void closeOnCompletion() throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertFalse(statement.isCloseOnCompletion()); - statement.closeOnCompletion(); - assertTrue(statement.isCloseOnCompletion()); - } - - @Test - void isPoolable() throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertFalse(statement.isPoolable()); - assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setPoolable(true)); - assertFalse(statement.isPoolable()); - assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setPoolable(false)); - assertFalse(statement.isPoolable()); - } - - @Test - void fetchSize() throws SQLException { - Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); - assertEquals(0, statement.getFetchSize()); - statement.setFetchSize(123); // ignore - assertEquals(0, statement.getFetchSize()); - assertThrows(SQLException.class, () -> statement.setFetchSize(-1)); - } - - @Test - void getConnection() throws SQLException { - FireboltConnection connection = mock(FireboltConnection.class); - Statement statement = new FireboltStatement(fireboltStatementService, null, connection); - assertSame(connection, statement.getConnection()); - } -} + @Captor + ArgumentCaptor queryInfoWrapperArgumentCaptor; + @Mock + private FireboltStatementService fireboltStatementService; + @Mock + private FireboltConnection fireboltConnection; + + private static FireboltStatement statement; + private final FireboltProperties fireboltProperties = FireboltProperties.builder() + .initialAdditionalProperties(new HashMap<>()) + .bufferSize(((Number)FireboltSessionProperty.BUFFER_SIZE.getDefaultValue()).intValue()) + .build(); + + private static Stream unsupported() { + return Stream.of( + Arguments.of("setCursorName", (Executable) () -> statement.setCursorName("my_cursor")), + Arguments.of("getGeneratedKeys", (Executable) () -> statement.getGeneratedKeys()), + Arguments.of("executeUpdate(column indexes)", (Executable) () -> statement.executeUpdate("insert", new int[] {1})), + Arguments.of("executeUpdate(column names)", (Executable) () -> statement.executeUpdate("insert", new String[] {"foo"})), + Arguments.of("execute(auto generated keys)", (Executable) () -> statement.execute("insert", Statement.RETURN_GENERATED_KEYS)), + Arguments.of("execute(column indexes)", (Executable) () -> statement.execute("insert", new int[] {1})), + Arguments.of("execute(column names)", (Executable) () -> statement.execute("insert", new String[] {"foo"})), + Arguments.of("execute(auto generated keys)", (Executable) () -> statement.execute("insert", Statement.RETURN_GENERATED_KEYS)), + Arguments.of("setPoolable(true)", (Executable) () -> statement.setPoolable(true)) + ); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("unsupported") + void shouldThrowSQLFeatureNotSupportedException(String name, Executable function) { + statement = new FireboltStatement(fireboltStatementService, mock(FireboltProperties.class), mock(FireboltConnection.class)); + assertThrows(SQLFeatureNotSupportedException.class, function); + } + + @Test + void shouldExtractAdditionalPropertiesAndNotExecuteQueryWhenSetParamIsUsed() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + try (FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection)) { + fireboltStatement.execute("set custom_1 = 1"); + verifyNoMoreInteractions(fireboltStatementService); + verify(connection).addProperty(Map.entry("custom_1", "1")); + } + } + + @Test + void shouldAbortStatementOnCancel() throws SQLException, ReflectiveOperationException { + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, fireboltConnection); + + Field runningStatementField = FireboltStatement.class.getDeclaredField("runningStatementLabel"); + runningStatementField.setAccessible(true); + runningStatementField.set(fireboltStatement, "1234"); + fireboltStatement.cancel(); + verify(fireboltStatementService).abortStatement(any(), eq(fireboltProperties)); + } + + @Test + void shouldCloseInputStreamOnClose() throws SQLException { + ResultSet rs = mock(ResultSet.class); + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.empty()); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.of(rs)); + fireboltStatement.executeQuery("show database"); + fireboltStatement.close(); + verify(rs).close(); + verify(connection).removeClosedStatement(fireboltStatement); + + // validate that recurrent close does not create cascading closing calls. + fireboltStatement.close(); + verifyNoMoreInteractions(connection); + } + + @Test + void shouldThrowAnExceptionWhenExecutingQueryOnANonQueryStatement() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + try (FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection)) { + assertThrows(FireboltException.class, () -> fireboltStatement.executeQuery("set custom_1 = 1")); + } + } + + @Test + void shouldExecuteIfUpdateStatementWouldNotReturnAResultSet() throws SQLException { + shouldExecuteIfUpdateStatementWouldNotReturnAResultSet(Statement::executeUpdate); + } + + @Test + void shouldExecuteIfUpdateStatementWouldNotReturnAResultSetNoGeneratedKeys() throws SQLException { + shouldExecuteIfUpdateStatementWouldNotReturnAResultSet((fireboltStatement, sql) -> fireboltStatement.executeUpdate(sql, Statement.NO_GENERATED_KEYS)); + } + + @Test + void shouldExecuteIfUpdateStatementWithIndexesWouldNotReturnAResultSet() throws SQLException { + shouldExecuteIfUpdateStatementWouldNotReturnAResultSet((fireboltStatement, sql) -> fireboltStatement.executeUpdate(sql, new int[0])); + } + + @Test + void shouldExecuteIfUpdateStatementWithNamesWouldNotReturnAResultSet() throws SQLException { + shouldExecuteIfUpdateStatementWouldNotReturnAResultSet((fireboltStatement, sql) -> fireboltStatement.executeUpdate(sql, new String[0])); + } + + private void shouldExecuteIfUpdateStatementWouldNotReturnAResultSet(CheckedBiFunction executor) throws SQLException { + try (FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, fireboltConnection)) { + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.empty()); + assertEquals(0, executor.apply(fireboltStatement, "INSERT INTO cars(sales, name) VALUES (500, 'Ford')")); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(fireboltProperties), any()); + assertEquals("INSERT INTO cars(sales, name) VALUES (500, 'Ford')", + queryInfoWrapperArgumentCaptor.getValue().getSql()); + assertEquals(0, fireboltStatement.getUpdateCount()); + } + } + + @Test + void shouldExecuteStatementThatReturnsResultSet() throws SQLException { + shouldExecuteStatementThatReturnsResultSet(Statement::execute); + } + + @Test + void shouldExecuteStatementNoGeneratedKeysThatReturnsResultSet() throws SQLException { + shouldExecuteStatementThatReturnsResultSet((fireboltStatement, sql) -> fireboltStatement.execute(sql, Statement.NO_GENERATED_KEYS)); + } + + @Test + void shouldExecuteStatementWithIndexesWouldNotReturnAResultSet() throws SQLException { + shouldExecuteStatementThatReturnsResultSet((fireboltStatement, sql) -> fireboltStatement.execute(sql, new int[0])); + } + + @Test + void shouldExecuteStatementWithNamesWouldNotReturnAResultSet() throws SQLException { + shouldExecuteStatementThatReturnsResultSet((fireboltStatement, sql) -> fireboltStatement.execute(sql, new String[0])); + } + + + private void shouldExecuteStatementThatReturnsResultSet(CheckedBiFunction executor) throws SQLException { + ResultSet rs = mock(FireboltResultSet.class); + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.of(rs)); + assertTrue(executor.apply(fireboltStatement, "SELECT 1")); + assertEquals(rs, fireboltStatement.getResultSet()); + assertEquals(-1, fireboltStatement.getUpdateCount()); + assertFalse(fireboltStatement.getMoreResults()); + verify(rs).close(); + assertNull(fireboltStatement.getResultSet()); + } + + @Test + void shouldCloseCurrentAndGetMoreResultsForMultiStatementQuery() throws SQLException { + ResultSet rs = mock(FireboltResultSet.class); + ResultSet rs2 = mock(FireboltResultSet.class); + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.of(rs)).thenReturn(Optional.of(rs2)); + fireboltStatement.execute("SELECT 1; SELECT 2;"); + assertEquals(rs, fireboltStatement.getResultSet()); + assertEquals(-1, fireboltStatement.getUpdateCount()); + fireboltStatement.getMoreResults(); + verify(rs).close(); + assertEquals(rs2, fireboltStatement.getResultSet()); + rs = fireboltStatement.getResultSet(); + fireboltStatement.getMoreResults(); + verify(rs).close(); + assertEquals(-1, fireboltStatement.getUpdateCount()); + assertNull(fireboltStatement.getResultSet()); + } + + + @Test + void test() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + + List logMessages = new ArrayList<>(); + Logger log = Logger.getLogger(FireboltStatement.class.getName()); + log.setLevel(Level.ALL); + log.addHandler(new Handler() { + @Override + public void publish(LogRecord record) { + logMessages.add(new MessageFormat(record.getMessage()).format(record.getParameters())); + } + + @Override + public void flush() { + } + + @Override + public void close() throws SecurityException { + } + }); + + String query = "SELECT 1"; + // This trick simulates cancelled statement. getLabel() called first time returns the initial label generated + // when StatementInfoWrapper is created. This value is stored in a collection of currently running queries. + // But next invocation of getLabel() will return other value, so the statement will not be found in list of running + // queries and will be considered as cancelled. + // It is ugly trick, but I do not know better way to simulate cancelled query. + StatementInfoWrapper statementInfoWrapper = new StatementInfoWrapper(query, StatementType.QUERY, null, new QueryRawStatement(query, query, List.of())) { + String label = null; + @Override + public String getLabel() { + if (label != null) { + return label; + } + String currentLabel = super.getLabel(); + label = "other label"; + return currentLabel; + } + }; + fireboltStatement.execute(List.of(statementInfoWrapper)); + assertNull(fireboltStatement.getResultSet()); + fireboltStatement.getMoreResults(CLOSE_CURRENT_RESULT); + verify(fireboltStatementService, times(0)).execute(any(), any(), any()); + assertTrue(logMessages.contains("Aborted query with id other label"), "Expected log message is not found"); + } + + + @Test + void shouldCloseCurrentAndGetMoreResultWhenCallingGetMoreResultsWithCloseCurrentFlag() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.of(mock(FireboltResultSet.class))); + fireboltStatement.execute("SELECT 1; SELECT 2;"); + ResultSet resultSet = fireboltStatement.getResultSet(); + fireboltStatement.getMoreResults(CLOSE_CURRENT_RESULT); + verify(resultSet).close(); + } + + @Test + void shouldKeepCurrentAndGetMoreResultWhenCallingGetMoreResultsWithKeepCurrentResultFlag() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.of(mock(ResultSet.class))); + fireboltStatement.execute("SELECT 1; SELECT 2;"); + ResultSet resultSet = fireboltStatement.getResultSet(); + fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); + verify(resultSet, never()).close(); + } + + @Test + void shouldCloseUnclosedAndGetMoreResultWhenCallingGetMoreResultsWithCloseAllResultFlag() throws SQLException { + ResultSet rs = mock(FireboltResultSet.class); + ResultSet rs2 = mock(FireboltResultSet.class); + ResultSet rs3 = mock(FireboltResultSet.class); + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + + when(fireboltStatementService.execute(any(), any(), any())) + .thenReturn(Optional.of(rs)).thenReturn(Optional.of(rs2)).thenReturn(Optional.of(rs3)); + + fireboltStatement.execute("SELECT 1; SELECT 2; SELECT 3;"); + ResultSet firstRs = fireboltStatement.getResultSet(); + fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); + verify(firstRs, never()).close(); + ResultSet secondRs = fireboltStatement.getResultSet(); + fireboltStatement.getMoreResults(Statement.KEEP_CURRENT_RESULT); + verify(secondRs, never()).close(); + fireboltStatement.getMoreResults(Statement.CLOSE_ALL_RESULTS); + verify(firstRs).close(); + verify(secondRs).close(); + + } + + @Test + void maxRows() throws SQLException { + try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { + assertEquals(0, statement.getMaxRows()); // zero means there is no limit + statement.setMaxRows(123); + assertEquals(123, statement.getMaxRows()); + } + } + + @Test + void negativeMaxRows() throws SQLException { + try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { + assertThrows(SQLException.class, () -> statement.setMaxRows(-1)); + } + } + + @Test + void statementParameters() throws SQLException { + try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { + assertEquals(ResultSet.FETCH_FORWARD, statement.getFetchDirection()); + assertEquals(ResultSet.CONCUR_READ_ONLY, statement.getResultSetConcurrency()); + assertEquals(ResultSet.TYPE_FORWARD_ONLY, statement.getResultSetType()); + assertEquals(ResultSet.HOLD_CURSORS_OVER_COMMIT, statement.getResultSetHoldability()); + assertNull(statement.getWarnings()); + } + } + + @Test + void setEscapeProcessing() throws SQLException { + try (Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class))) { + assertNull(statement.getWarnings()); + statement.setEscapeProcessing(false); + assertNull(statement.getWarnings()); + // set escape processing and get 1 warning + statement.setEscapeProcessing(true); + SQLWarning firstWarning = statement.getWarnings(); + assertNotNull(firstWarning); + assertNull(firstWarning.getNextWarning()); + + // now do it again. Now we get 2 warnings: first one - the same, and yet another one - chained + statement.setEscapeProcessing(true); + assertSame(firstWarning, statement.getWarnings()); + assertNotNull(firstWarning.getNextWarning()); + assertNull(firstWarning.getNextWarning().getNextWarning()); + + // now disable escape processing. The warnings remain the same + statement.setEscapeProcessing(false); + assertSame(firstWarning, statement.getWarnings()); + assertNotNull(firstWarning.getNextWarning()); + assertNull(firstWarning.getNextWarning().getNextWarning()); + + // clean warnings and validate them. + statement.clearWarnings(); + assertNull(statement.getWarnings()); + } + } + + @ParameterizedTest + @ValueSource(classes = {Statement.class, FireboltStatement.class, Wrapper.class, AutoCloseable.class}) + void successfulUnwrap(Class clazz) throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertSame(statement, statement.unwrap(clazz)); + } + + @ParameterizedTest + @ValueSource(classes = {Connection.class, String.class, Closeable.class}) + void failingUnwrap(Class clazz) { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertThrows(SQLException.class, () -> statement.unwrap(clazz)); + } + + @ParameterizedTest + @ValueSource(classes = {Statement.class, FireboltStatement.class, Wrapper.class, AutoCloseable.class}) + void isWrapperFor(Class clazz) throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertTrue(statement.isWrapperFor(clazz)); + } + + @ParameterizedTest + @ValueSource(classes = {Connection.class, String.class, Closeable.class}) + void isNotWrapperFor(Class clazz) throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertFalse(statement.isWrapperFor(clazz)); + } + + @Test + void queryTimeout() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertEquals(0, statement.getQueryTimeout()); + statement.setQueryTimeout(12345); + assertEquals(12345, statement.getQueryTimeout()); + } + + @Test + void closeOnCompletion() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertFalse(statement.isCloseOnCompletion()); + statement.closeOnCompletion(); + assertTrue(statement.isCloseOnCompletion()); + } + + @Test + void isPoolable() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertFalse(statement.isPoolable()); + assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setPoolable(true)); + assertFalse(statement.isPoolable()); + assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setPoolable(false)); + assertFalse(statement.isPoolable()); + } + + @Test + void fetchSize() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertEquals(0, statement.getFetchSize()); + statement.setFetchSize(123); // ignore + assertEquals(0, statement.getFetchSize()); + assertThrows(SQLException.class, () -> statement.setFetchSize(-1)); + } + + @Test + void fetchDirection() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertEquals(ResultSet.FETCH_FORWARD, statement.getFetchDirection()); // check initial value + + // set the same value; should succeed + statement.setFetchDirection(ResultSet.FETCH_FORWARD); + assertEquals(ResultSet.FETCH_FORWARD, statement.getFetchDirection()); + + // set wrong values expecting exceptions + assertThrows(SQLException.class, () -> statement.setFetchDirection(ResultSet.FETCH_REVERSE)); + assertThrows(SQLException.class, () -> statement.setFetchDirection(ResultSet.FETCH_UNKNOWN)); + assertThrows(SQLException.class, () -> statement.setFetchDirection(999999)); + // check that returned value is still FETCH_FORWARD + assertEquals(ResultSet.FETCH_FORWARD, statement.getFetchDirection()); + } + + @Test + void maxFieldSize() throws SQLException { + Statement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)); + assertEquals(0, statement.getMaxFieldSize()); + statement.setMaxFieldSize(123); + assertEquals(123, statement.getMaxFieldSize()); + } + + @Test + void getConnection() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + Statement statement = new FireboltStatement(fireboltStatementService, null, connection); + assertSame(connection, statement.getConnection()); + } + + @Test + void warnings() throws SQLException { + FireboltStatement statement = new FireboltStatement(fireboltStatementService, null, mock(FireboltConnection.class)) { + { + addWarning(new SQLWarning("test")); + } + }; + SQLWarning warning = statement.getWarnings(); + assertNotNull(warning); + assertEquals("test", warning.getMessage()); + statement.clearWarnings(); + assertNull(statement.getWarnings()); + } + + @ParameterizedTest + @CsvSource(value = { + "hello,5,hello", + "hello,6,hello", + "hello,0,hello", + "hello,-1,hello", + "hello,4,hell", + "hello,2,he", + "hello,1,h", + ",-1,", + ",0,", + ",1,", + }) + void shouldLimitStringByMaxFieldSize(String inputText, int maxFieldSize, String expectedText) throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + StatementClient statementClient = mock(StatementClient.class); + String content = format("1\t2\ntext\tbytea\n%s\t%s", inputText == null ? "\\N" : inputText, inputText == null ? "\\N" : SqlArrayUtil.byteArrayToHexString(inputText.getBytes(), false)); + when(statementClient.executeSqlStatement(any(), any(), eq(false), anyInt())).thenReturn(new ByteArrayInputStream(content.getBytes())); + FireboltStatementService statementService = new FireboltStatementService(statementClient); + FireboltStatement fireboltStatement = new FireboltStatement(statementService, fireboltProperties, connection); + when(connection.createStatement()).thenReturn(fireboltStatement); + fireboltStatement.setMaxFieldSize(maxFieldSize); + ResultSet rs = connection.createStatement().executeQuery("SELECT word from table"); + assertTrue(rs.next()); + + assertEquals(expectedText, rs.getString(1)); + assertEquals(expectedText, rs.getObject(1)); + + if (expectedText == null) { + assertNull(rs.getString(2)); + assertNull(rs.getObject(2)); + } else { + String expectedHexText = SqlArrayUtil.byteArrayToHexString(expectedText.getBytes(), false); + assertEquals(expectedHexText, rs.getString(2)); + assertArrayEquals(expectedText.getBytes(), (byte[]) rs.getObject(2)); + } + + assertFalse(rs.next()); + } + + @Test + void shouldExecuteEmptyBatch() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + assertArrayEquals(new int[0], fireboltStatement.executeBatch()); + } + + @Test + void shouldExecuteBatch() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn( + Optional.of(mock(FireboltResultSet.class)), Optional.of(mock(FireboltResultSet.class)), Optional.empty(), Optional.empty(), Optional.of(mock(FireboltResultSet.class)) + ); + + fireboltStatement.addBatch("SELECT 1; SELECT 2;"); + fireboltStatement.addBatch("INSERT INTO PEOPLE (id, name) VALUES (1, 'Adam')"); + fireboltStatement.addBatch("INSERT INTO PEOPLE (id, name) VALUES (1, 'Eve')"); + fireboltStatement.addBatch("SELECT 3"); + + int[] actual = fireboltStatement.executeBatch(); + assertArrayEquals(new int[] {0, 0, SUCCESS_NO_INFO, SUCCESS_NO_INFO, 0}, actual); + } + + @Test + void shouldClearBatch() throws SQLException { + FireboltConnection connection = mock(FireboltConnection.class); + FireboltStatement fireboltStatement = new FireboltStatement(fireboltStatementService, fireboltProperties, connection); + when(fireboltStatementService.execute(any(), any(), any())).thenReturn( + Optional.empty(), Optional.empty() + ); + + fireboltStatement.addBatch("SELECT 1; SELECT 2;"); + fireboltStatement.clearBatch(); + assertArrayEquals(new int[0], fireboltStatement.executeBatch()); + + fireboltStatement.addBatch("INSERT INTO PEOPLE (id, name) VALUES (1, 'Adam')"); + fireboltStatement.addBatch("INSERT INTO PEOPLE (id, name) VALUES (1, 'Eve')"); + + assertArrayEquals(new int[] {SUCCESS_NO_INFO, SUCCESS_NO_INFO}, fireboltStatement.executeBatch()); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatementTest.java b/src/test/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatementTest.java index e6137dd0..09f3418c 100644 --- a/src/test/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatementTest.java +++ b/src/test/java/com/firebolt/jdbc/statement/preparedstatement/FireboltPreparedStatementTest.java @@ -1,5 +1,6 @@ package com.firebolt.jdbc.statement.preparedstatement; +import com.firebolt.jdbc.client.query.StatementClient; import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.connection.settings.FireboltProperties; import com.firebolt.jdbc.exception.FireboltException; @@ -7,6 +8,8 @@ import com.firebolt.jdbc.statement.StatementInfoWrapper; import com.firebolt.jdbc.type.FireboltDataType; import com.firebolt.jdbc.type.array.FireboltArray; +import com.firebolt.jdbc.type.lob.FireboltBlob; +import com.firebolt.jdbc.type.lob.FireboltClob; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,7 +17,9 @@ import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.junitpioneer.jupiter.DefaultTimeZone; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -22,7 +27,10 @@ import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.io.StringReader; import java.math.BigDecimal; import java.net.URL; @@ -30,6 +38,7 @@ import java.sql.Clob; import java.sql.Date; import java.sql.NClob; +import java.sql.PreparedStatement; import java.sql.Ref; import java.sql.RowId; import java.sql.SQLException; @@ -38,20 +47,27 @@ import java.sql.Time; import java.sql.Timestamp; import java.sql.Types; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Optional; +import java.util.TimeZone; import java.util.stream.Stream; +import static java.lang.String.format; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.anyBoolean; -import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class FireboltPreparedStatementTest { @@ -66,56 +82,126 @@ class FireboltPreparedStatementTest { private final FireboltConnection connection = Mockito.mock(FireboltConnection.class); private static FireboltPreparedStatement statement; + private interface Setter { + void set(PreparedStatement ps) throws SQLException; + } + + private interface ParameterizedSetter { + void set(PreparedStatement statement, int index, T value) throws SQLException; + } + private static Stream unsupported() { return Stream.of( - Arguments.of("setByte", (Executable) () -> statement.setByte(1, (byte) 127)), - Arguments.of("setBytes", (Executable) () -> statement.setBytes(1, "bytes".getBytes())), - Arguments.of("setURL", (Executable) () -> statement.setURL(1, new URL("http://foo.bar"))), - Arguments.of("setCharacterStream", (Executable) () -> statement.setCharacterStream(1, new StringReader("hello"))), - Arguments.of("setCharacterStream(length)", (Executable) () -> statement.setCharacterStream(1, new StringReader("hello"), 2)), - Arguments.of("setCharacterStream(long length)", (Executable) () -> statement.setCharacterStream(1, new StringReader("hello"), 2L)), - Arguments.of("setNCharacterStream", (Executable) () -> statement.setNCharacterStream(1, new StringReader("hello"))), - Arguments.of("setNCharacterStream(length)", (Executable) () -> statement.setNCharacterStream(1, new StringReader("hello"), 2)), - Arguments.of("setNCharacterStream(length)", (Executable) () -> statement.setNCharacterStream(1, new StringReader("hello"), 2L)), - Arguments.of("setRef", (Executable) () -> statement.setRef(1, mock(Ref.class))), - Arguments.of("setBlob", (Executable) () -> statement.setBlob(1, mock(Blob.class))), - Arguments.of("setBlob(input stream)", (Executable) () -> statement.setBlob(1, mock(InputStream.class))), - Arguments.of("setBlob(input stream, length)", (Executable) () -> statement.setBlob(1, mock(InputStream.class), 123)), - Arguments.of("setClob", (Executable) () -> statement.setClob(1, mock(Clob.class))), - Arguments.of("setClob(reader)", (Executable) () -> statement.setClob(1, new StringReader("hello"))), - Arguments.of("setClob(reader, length)", (Executable) () -> statement.setClob(1, new StringReader("hello"), 1)), - Arguments.of("setNClob", (Executable) () -> statement.setNClob(1, mock(NClob.class))), - Arguments.of("setNClob(reader)", (Executable) () -> statement.setNClob(1, new StringReader("hello"))), - Arguments.of("setNClob(reader, length)", (Executable) () -> statement.setNClob(1, new StringReader("hello"), 1L)), - - Arguments.of("setAsciiStream", (Executable) () -> statement.setAsciiStream(1, mock(InputStream.class))), - Arguments.of("setAsciiStream(int length)", (Executable) () -> statement.setAsciiStream(1, mock(InputStream.class), 456)), - Arguments.of("setAsciiStream(long length)", (Executable) () -> statement.setAsciiStream(1, mock(InputStream.class), 456L)), - Arguments.of("setBinaryStream", (Executable) () -> statement.setBinaryStream(1, mock(InputStream.class))), - Arguments.of("setBinaryStream(int length)", (Executable) () -> statement.setBinaryStream(1, mock(InputStream.class), 456)), - Arguments.of("setBinaryStream(long length)", (Executable) () -> statement.setBinaryStream(1, mock(InputStream.class), 456L)), - Arguments.of("setUnicodeStream(int length)", (Executable) () -> statement.setUnicodeStream(1, mock(InputStream.class), 123)), - - Arguments.of("setDate", (Executable) () -> statement.setDate(1, new Date(System.currentTimeMillis()), Calendar.getInstance())), Arguments.of("setTime", (Executable) () -> statement.setTime(1, new Time(System.currentTimeMillis()))), Arguments.of("setTime(calendar)", (Executable) () -> statement.setTime(1, new Time(System.currentTimeMillis()), Calendar.getInstance())), - Arguments.of("setTimestamp", (Executable) () -> statement.setTimestamp(1, new Timestamp(System.currentTimeMillis()), Calendar.getInstance())), - Arguments.of("setRowId", (Executable) () -> statement.setRowId(1, mock(RowId.class)), + Arguments.of("setRowId", (Executable) () -> statement.setRowId(1, mock(RowId.class))), Arguments.of("setSQLXML", (Executable) () -> statement.setSQLXML(1, mock(SQLXML.class))), - // TODO: add support of this method - Arguments.of("getParameterMetaData", (Executable) () -> statement.getParameterMetaData())), - Arguments.of("setObject", (Executable) () -> statement.setObject(1, mock(SQLXML.class), Types.VARCHAR, 0)), + // TODO: add support of these methods Arguments.of("getParameterMetaData", (Executable) () -> statement.getParameterMetaData()) ); } + private static Stream buffers() { + return Stream.of( + Arguments.of("setClob((Clob)null)", (Setter) statement -> statement.setClob(1, (Clob)null), "NULL"), + Arguments.of("setClob((Reader)null)", (Setter) statement -> statement.setClob(1, (Reader)null), "NULL"), + Arguments.of("setClob((Reader)null, length)", (Setter) statement -> statement.setClob(1, null, 1L), "NULL"), + Arguments.of("setClob(Reader)", (Setter) statement -> statement.setClob(1, new FireboltClob("hello".toCharArray())), "'hello'"), + Arguments.of("setClob(Reader, length=)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 5), "'hello'"), + Arguments.of("setClob(Reader, length-1)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 4), "'hell'"), + Arguments.of("setClob(Reader, length+1)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 6), "'hello'"), + Arguments.of("setClob(Reader, 42)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 42), "'hello'"), + Arguments.of("setClob(Reader, 1)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 1), "'h'"), + Arguments.of("setClob(Reader, 0)", (Setter) statement -> statement.setClob(1, new StringReader("hello"), 0), "''"), + + Arguments.of("setNClob((NClob)null)", (Setter) statement -> statement.setNClob(1, (NClob)null), "NULL"), + Arguments.of("setNClob((Reader)null)", (Setter) statement -> statement.setNClob(1, (Reader)null), "NULL"), + Arguments.of("setNClob((Reader)null, length)", (Setter) statement -> statement.setNClob(1, null, 1L), "NULL"), + Arguments.of("setClob(Reader)", (Setter) statement -> statement.setNClob(1, new FireboltClob("hello".toCharArray())), "'hello'"), + Arguments.of("setNClob(Reader, length=)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 5), "'hello'"), + Arguments.of("setNClob(Reader, length-1)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 4), "'hell'"), + Arguments.of("setNClob(Reader, length+1)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 6), "'hello'"), + Arguments.of("setNClob(Reader, 42)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 42), "'hello'"), + Arguments.of("setNClob(Reader, 1)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 1), "'h'"), + Arguments.of("setNClob(Reader, 0)", (Setter) statement -> statement.setNClob(1, new StringReader("hello"), 0), "''"), + + Arguments.of("setBlob((Blob)null)", (Setter) statement -> statement.setBlob(1, (Blob)null), "NULL"), + Arguments.of("setBClob((InputStream)null)", (Setter) statement -> statement.setBlob(1, (InputStream)null), "NULL"), + Arguments.of("setBClob((InputStream)null, length)", (Setter) statement -> statement.setBlob(1, null, 1L), "NULL"), + Arguments.of("setBlob((Clob)null)", (Setter) statement -> statement.setBlob(1, new FireboltBlob("hello".getBytes())), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + + Arguments.of("setCharacterStream(null)", (Setter) statement -> statement.setCharacterStream(1, null), "NULL"), + Arguments.of("setCharacterStream(null, int)", (Setter) statement -> statement.setCharacterStream(1, null, 1), "NULL"), + Arguments.of("setCharacterStream(null, long)", (Setter) statement -> statement.setCharacterStream(1, null, 1L), "NULL"), + Arguments.of("setCharacterStream(Reader)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello")), "'hello'"), + Arguments.of("setCharacterStream(Reader, length=)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 5), "'hello'"), + Arguments.of("setCharacterStream(Reader, length-1)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 4), "'hell'"), + Arguments.of("setCharacterStream(Reader, length+1)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 6), "'hello'"), + Arguments.of("setCharacterStream(Reader, 42)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 42), "'hello'"), + Arguments.of("setCharacterStream(Reader, 1)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 1), "'h'"), + Arguments.of("setCharacterStream(Reader, 0)", (Setter) statement -> statement.setCharacterStream(1, new StringReader("hello"), 0), "''"), + + Arguments.of("setNCharacterStream(null)", (Setter) statement -> statement.setNCharacterStream(1, null), "NULL"), + Arguments.of("setNCharacterStream(null, int)", (Setter) statement -> statement.setNCharacterStream(1, null, 1), "NULL"), + Arguments.of("setNCharacterStream(null, long)", (Setter) statement -> statement.setNCharacterStream(1, null, 1L), "NULL"), + Arguments.of("setNCharacterStream(Reader)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello")), "'hello'"), + Arguments.of("setNCharacterStream(Reader, length=)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 5), "'hello'"), + Arguments.of("setNCharacterStream(Reader, length-1)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 4), "'hell'"), + Arguments.of("setNCharacterStream(Reader, length+1)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 6), "'hello'"), + Arguments.of("setNCharacterStream(Reader, 42)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 42), "'hello'"), + Arguments.of("setNCharacterStream(Reader, 1)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 1), "'h'"), + Arguments.of("setNCharacterStream(Reader, 0)", (Setter) statement -> statement.setNCharacterStream(1, new StringReader("hello"), 0), "''"), + + Arguments.of("setAsciiStream(null)", (Setter) statement -> statement.setAsciiStream(1, null), "NULL"), + Arguments.of("setAsciiStream(null, int)", (Setter) statement -> statement.setAsciiStream(1, null, 1), "NULL"), + Arguments.of("setAsciiStream(null, long)", (Setter) statement -> statement.setAsciiStream(1, null, 1L), "NULL"), + Arguments.of("setAsciiStream(InputStream)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes())), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, length=)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 5), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, length-1)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 4), "E'\\x68\\x65\\x6c\\x6c'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, length+1)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 6), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, 42)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 42), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, 1)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 1), "E'\\x68'::BYTEA"), + Arguments.of("setAsciiStream(InputStream, 0)", (Setter) statement -> statement.setAsciiStream(1, new ByteArrayInputStream("hello".getBytes()), 0), "E'\\x'::BYTEA"), + + Arguments.of("setBinaryStream(null)", (Setter) statement -> statement.setBinaryStream(1, null), "NULL"), + Arguments.of("setBinaryStream(null, int)", (Setter) statement -> statement.setBinaryStream(1, null, 1), "NULL"), + Arguments.of("setBinaryStream(null, long)", (Setter) statement -> statement.setBinaryStream(1, null, 1L), "NULL"), + Arguments.of("setBinaryStream(InputStream)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes())), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, length=)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 5), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, length-1)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 4), "E'\\x68\\x65\\x6c\\x6c'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, length+1)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 6), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, 42)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 42), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, 1)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 1), "E'\\x68'::BYTEA"), + Arguments.of("setBinaryStream(InputStream, 0)", (Setter) statement -> statement.setBinaryStream(1, new ByteArrayInputStream("hello".getBytes()), 0), "E'\\x'::BYTEA"), + + Arguments.of("setUnicodeStream(null, int)", (Setter) statement -> statement.setUnicodeStream(1, null, 1), "NULL"), + Arguments.of("setUnicodeStream(InputStream, length=)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 5), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setUnicodeStream(InputStream, length-1)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 4), "E'\\x68\\x65\\x6c\\x6c'::BYTEA"), + Arguments.of("setUnicodeStream(InputStream, length+1)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 6), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setUnicodeStream(InputStream, 42)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 42), "E'\\x68\\x65\\x6c\\x6c\\x6f'::BYTEA"), + Arguments.of("setUnicodeStream(InputStream, 1)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 1), "E'\\x68'::BYTEA"), + Arguments.of("setUnicodeStream(InputStream, 0)", (Setter) statement -> statement.setUnicodeStream(1, new ByteArrayInputStream("hello".getBytes()), 0), "E'\\x'::BYTEA")); + } + + private static Stream setNumber() { + return Stream.of( + Arguments.of("byte", (ParameterizedSetter) PreparedStatement::setByte, (byte)50), + Arguments.of("short", (ParameterizedSetter) PreparedStatement::setShort, (short)50), + Arguments.of("int", (ParameterizedSetter) PreparedStatement::setInt, 50), + Arguments.of("long", (ParameterizedSetter) PreparedStatement::setLong, 50L), + Arguments.of("float", (ParameterizedSetter) PreparedStatement::setFloat, 5.5f), + Arguments.of("double", (ParameterizedSetter) PreparedStatement::setDouble, 3.14), + Arguments.of("double", (ParameterizedSetter) PreparedStatement::setBigDecimal, new BigDecimal("555555555555.55555555")) + ); + } + @BeforeEach void beforeEach() throws SQLException { + when(connection.getSessionProperties()).thenReturn(properties); lenient().when(properties.getBufferSize()).thenReturn(10); - lenient().when(fireboltStatementService.execute(any(), any(), anyInt(), anyInt(), anyBoolean(), any())) - .thenReturn(Optional.empty()); + lenient().when(fireboltStatementService.execute(any(), any(), any())).thenReturn(Optional.empty()); } @AfterEach @@ -125,9 +211,29 @@ void afterEach() throws SQLException { } } + @ParameterizedTest + @CsvSource(value = { + "INSERT INTO data (field) VALUES (?),false", + "SELECT * FROM data WHERE field=?,true", + }) + void getMetadata(String query, boolean expectedResultSet) throws SQLException { + StatementClient statementClient = mock(StatementClient.class); + when(statementClient.executeSqlStatement(any(), any(), anyBoolean(), anyInt())).thenReturn(new ByteArrayInputStream(new byte[0])); + statement = new FireboltPreparedStatement(new FireboltStatementService(statementClient), connection, query); + assertNull(statement.getMetaData()); + statement.setObject(1, null); + boolean shouldHaveResultSet = statement.execute(); + assertEquals(expectedResultSet, shouldHaveResultSet); + if (shouldHaveResultSet) { + assertNotNull(statement.getMetaData()); + } else { + assertNull(statement.getMetaData()); + } + } + @Test void shouldExecute() throws SQLException { - statement = createStatementWithSql("INSERT INTO cars (sales, make, model, minor_model, color, type, types) VALUES (?,?,?,?,?,?,?)"); + statement = createStatementWithSql("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (?,?,?,?,?,?,?,?)"); statement.setInt(1, 500); statement.setString(2, "Ford"); @@ -136,14 +242,65 @@ void shouldExecute() throws SQLException { statement.setNull(5, Types.VARCHAR, "VARCHAR"); statement.setNString(6, "sedan"); statement.setArray(7, new FireboltArray(FireboltDataType.TEXT, new String[] {"sedan", "hatchback", "coupe"})); + statement.setBytes(8, "HarryFord".getBytes()); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (500,'Ford','FOCUS',NULL,NULL,'sedan',['sedan','hatchback','coupe'],E'\\x48\\x61\\x72\\x72\\x79\\x46\\x6f\\x72\\x64'::BYTEA)", + queryInfoWrapperArgumentCaptor.getValue().getSql()); + } - assertEquals("INSERT INTO cars (sales, make, model, minor_model, color, type, types) VALUES (500,'Ford','FOCUS',NULL,NULL,'sedan',['sedan','hatchback','coupe'])", + @Test + void setNullByteArray() throws SQLException { + statement = createStatementWithSql("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (?,?,?,?,?,?,?,?)"); + + statement.setShort(1, (short)500); + statement.setString(2, "Ford"); + statement.setObject(3, "FOCUS", Types.VARCHAR); + statement.setNull(4, Types.VARCHAR); + statement.setNull(5, Types.VARCHAR, "VARCHAR"); + statement.setNString(6, "sedan"); + statement.setArray(7, new FireboltArray(FireboltDataType.TEXT, new String[] {"sedan", "hatchback", "coupe"})); + statement.setBytes(8, null); + statement.execute(); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars (sales, make, model, minor_model, color, type, types, signature) VALUES (500,'Ford','FOCUS',NULL,NULL,'sedan',['sedan','hatchback','coupe'],NULL)", queryInfoWrapperArgumentCaptor.getValue().getSql()); } + @ParameterizedTest(name = "{0}") + @MethodSource("buffers") + void setBuffer(String name, Setter setter, String expected) throws SQLException { + statement = createStatementWithSql("INSERT INTO data (field) VALUES (?)"); + setter.set(statement); + statement.execute(); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals(format("INSERT INTO data (field) VALUES (%s)", expected), queryInfoWrapperArgumentCaptor.getValue().getSql()); + } + + @Test + void setFailingCharacterStream() throws IOException { + statement = createStatementWithSql("INSERT INTO data (field) VALUES (?)"); + Reader reader = mock(Reader.class); + when(reader.read(any(), anyInt(), anyInt())).thenThrow(new IOException()); + assertEquals(IOException.class, assertThrows(SQLException.class, () -> statement.setCharacterStream(1, reader)).getCause().getClass()); + } + + @Test + void setFailingBinaryStream() throws IOException { + statement = createStatementWithSql("INSERT INTO data (field) VALUES (?)"); + InputStream is = mock(InputStream.class); + when(is.readAllBytes()).thenThrow(new IOException()); + assertEquals(IOException.class, assertThrows(SQLException.class, () -> statement.setBinaryStream(1, is)).getCause().getClass()); + } + + @Test + void setFailingLimitedBinaryStream() throws IOException { + statement = createStatementWithSql("INSERT INTO data (field) VALUES (?)"); + InputStream is = mock(InputStream.class); + when(is.readNBytes(1024)).thenThrow(new IOException()); + assertEquals(IOException.class, assertThrows(SQLException.class, () -> statement.setBinaryStream(1, is, 1024)).getCause().getClass()); + } + @Test void shouldExecuteBatch() throws SQLException { statement = createStatementWithSql("INSERT INTO cars (sales, make) VALUES (?,?)"); @@ -155,29 +312,23 @@ void shouldExecuteBatch() throws SQLException { statement.setObject(2, "Tesla"); statement.addBatch(); statement.executeBatch(); - verify(fireboltStatementService, times(2)).execute(queryInfoWrapperArgumentCaptor.capture(), - eq(this.properties), anyInt(), anyInt(), anyBoolean(), any()); - + verify(fireboltStatementService, times(2)).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); assertEquals("INSERT INTO cars (sales, make) VALUES (150,'Ford')", queryInfoWrapperArgumentCaptor.getAllValues().get(0).getSql()); assertEquals("INSERT INTO cars (sales, make) VALUES (300,'Tesla')", queryInfoWrapperArgumentCaptor.getAllValues().get(1).getSql()); } - @Test - void shouldThrowExceptionWhenAllParametersAreNotDefined() { - FireboltPreparedStatement statementWithUndefinedParamWithSpace = FireboltPreparedStatement.statementBuilder() - .sql("SELECT * FROM cars WHERE make LIKE ?").build(); - - FireboltPreparedStatement statementWithUndefinedParamWithComma = FireboltPreparedStatement.statementBuilder() - .sql("SELECT * FROM cars WHERE make LIKE ,?").build(); - - FireboltPreparedStatement statementWithUndefinedParamWithParenthesis = FireboltPreparedStatement - .statementBuilder().sql("SELECT * FROM cars WHERE make LIKE (?").build(); - - assertThrows(IllegalArgumentException.class, statementWithUndefinedParamWithSpace::executeQuery); - assertThrows(IllegalArgumentException.class, statementWithUndefinedParamWithComma::executeQuery); - assertThrows(IllegalArgumentException.class, statementWithUndefinedParamWithParenthesis::executeQuery); + @ParameterizedTest + @ValueSource(strings = { + "SELECT * FROM cars WHERE make LIKE ?", + "SELECT * FROM cars WHERE make LIKE ,?", + "SELECT * FROM cars WHERE make LIKE (?" + }) + void shouldThrowExceptionWhenAllParametersAreNotDefined(String query) throws SQLException { + try (PreparedStatement ps = createStatementWithSql(query)) { + assertThrows(IllegalArgumentException.class, ps::executeQuery); + } } @Test @@ -191,8 +342,7 @@ void shouldExecuteWithSpecialCharactersInQuery() throws SQLException { String expectedSql = "INSERT INTO cars (model ,sales, make) VALUES ('?',' ?','(?:^|[^\\\\p{L}\\\\p{N}])(?i)(phone)(?:[^\\\\p{L}\\\\p{N}]|$)')"; statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); assertEquals(expectedSql, queryInfoWrapperArgumentCaptor.getValue().getSql()); } @@ -207,7 +357,22 @@ void shouldThrowExceptionWhenTooManyParametersAreProvided() throws SQLException } @Test - void shouldThrowsExceptionWhenTryingToExecuteUpdate() throws SQLException { + void shouldExecuteUpdate() throws SQLException { + statement = createStatementWithSql("update cars set sales = ? where make = ?"); + + statement.setObject(1, 150); + statement.setObject(2, "Ford"); + + assertEquals(0, statement.executeUpdate()); // we are not able to return number of affected lines right now + + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("update cars set sales = 150 where make = 'Ford'", + queryInfoWrapperArgumentCaptor.getValue().getSql()); + } + + + @Test + void shouldThrowsExceptionWhenTryingToExecuteUpdateWithQuery() throws SQLException { statement = createStatementWithSql("update cars set sales = ? where make = ?"); statement.setObject(1, 150); @@ -225,8 +390,7 @@ void shouldSetNull() throws SQLException { statement.setNull(2, 0); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); assertEquals("INSERT INTO cars (sales, make) VALUES (NULL,NULL)", queryInfoWrapperArgumentCaptor.getValue().getSql()); } @@ -238,16 +402,28 @@ void shouldSetBoolean() throws SQLException { statement.setBoolean(1, true); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); assertEquals("INSERT INTO cars(available) VALUES (1)", queryInfoWrapperArgumentCaptor.getValue().getSql()); } - @Test - void shouldThrowExceptionWhenTryingToSetCharacterStream() { - statement = createStatementWithSql("INSERT INTO cars(make) VALUES (?)"); - assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setCharacterStream(1, new StringReader("hello"))); - assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setCharacterStream(1, new StringReader("hello"), 2)); + @ParameterizedTest + @CsvSource(value = { + "Nobody,", + "Firebolt,http://www.firebolt.io" + }) + void shouldSetUrl(String name, URL url) throws SQLException { + statement = createStatementWithSql("INSERT INTO companies (name, url) VALUES (?,?)"); + + statement.setString(1, name); + statement.setURL(2, url); + statement.execute(); + + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals(format("INSERT INTO companies (name, url) VALUES (%s,%s)", sqlQuote(name), sqlQuote(url)), queryInfoWrapperArgumentCaptor.getValue().getSql()); + } + + private String sqlQuote(Object value) { + return value == null ? "NULL" : format("'%s'", value); } @ParameterizedTest(name = "{0}") @@ -257,92 +433,92 @@ void shouldThrowSQLFeatureNotSupportedException(String name, Executable function assertThrows(SQLFeatureNotSupportedException.class, function); } - @Test - void shouldSetInt() throws SQLException { + @ParameterizedTest(name = "{0}") + @MethodSource("setNumber") + void shouldSetNumber(String name, ParameterizedSetter setter, T value) throws SQLException { statement = createStatementWithSql("INSERT INTO cars(price) VALUES (?)"); - statement.setInt(1, 50); + setter.set(statement, 1, value); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); } @Test - void shouldSetLong() throws SQLException { - statement = createStatementWithSql("INSERT INTO cars(price) VALUES (?)"); + @DefaultTimeZone("Europe/London") + void shouldSetDate() throws SQLException { + statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); - statement.setLong(1, 50L); + statement.setDate(1, new Date(1564527600000L)); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("INSERT INTO cars(price) VALUES (50)", queryInfoWrapperArgumentCaptor.getValue().getSql()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31')", + queryInfoWrapperArgumentCaptor.getValue().getSql()); } @Test - void shouldSetFloat() throws SQLException { - statement = createStatementWithSql("INSERT INTO cars(price) VALUES (?)"); - - statement.setFloat(1, 5.5F); + void shouldSetDateWithCalendar() throws SQLException, ParseException { + statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST")); + calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:00:00 GMT")); + statement.setDate(1, new Date(calendar.getTimeInMillis()), calendar); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); - - assertEquals("INSERT INTO cars(price) VALUES (5.5)", queryInfoWrapperArgumentCaptor.getValue().getSql()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES ('2024-04-19')", + queryInfoWrapperArgumentCaptor.getValue().getSql()); } @Test - void shouldSetDouble() throws SQLException { - statement = createStatementWithSql("INSERT INTO cars(price) VALUES (?)"); + @DefaultTimeZone("Europe/London") + void shouldSetDateWithNullCalendar() throws SQLException, ParseException { + statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); - statement.setDouble(1, 5.5); + statement.setDate(1, new Date(1564527600000L), null); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31')", + queryInfoWrapperArgumentCaptor.getValue().getSql()); } @Test - void shouldSetBigDecimal() throws SQLException { - statement = createStatementWithSql("INSERT INTO cars(price) VALUES (?)"); - - statement.setBigDecimal(1, new BigDecimal("555555555555.55555555")); + void shouldSetTimeStampWithCalendar() throws SQLException, ParseException { + statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST")); + calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:11:01 GMT")); + statement.setTimestamp(1, new Timestamp(calendar.getTimeInMillis()), calendar); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("INSERT INTO cars(price) VALUES (555555555555.55555555)", + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES ('2024-04-19 05:11:01')", queryInfoWrapperArgumentCaptor.getValue().getSql()); } @Test @DefaultTimeZone("Europe/London") - void shouldSetDate() throws SQLException { + void shouldSetTimeStamp() throws SQLException { statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); - statement.setDate(1, new Date(1564527600000L)); + statement.setTimestamp(1, new Timestamp(1564571713000L)); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); - assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31')", + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31 12:15:13')", queryInfoWrapperArgumentCaptor.getValue().getSql()); } @Test - @DefaultTimeZone("Europe/London") - void shouldSetTimeStamp() throws SQLException { + void shouldSetNullTimeStampWithCalendar() throws SQLException, ParseException { statement = createStatementWithSql("INSERT INTO cars(release_date) VALUES (?)"); - - statement.setTimestamp(1, new Timestamp(1564571713000L)); + Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("JST")); + calendar.setTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z").parse("2024-04-18 20:11:01 GMT")); + statement.setTimestamp(1, null, calendar); statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); - - assertEquals("INSERT INTO cars(release_date) VALUES ('2019-07-31 12:15:13')", + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals("INSERT INTO cars(release_date) VALUES (NULL)", queryInfoWrapperArgumentCaptor.getValue().getSql()); } @@ -364,16 +540,131 @@ void shouldSetAllObjects() throws SQLException { statement.execute(); - verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(this.properties), - anyInt(), anyInt(), anyBoolean(), any()); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + assertEquals( + "INSERT INTO cars(timestamp, date, float, long, big_decimal, null, boolean, int) VALUES ('2019-07-31 12:15:13','2019-07-31',5.5,5,555555555555.55555555,NULL,1,5)", + queryInfoWrapperArgumentCaptor.getValue().getSql()); + } + + @Test + @DefaultTimeZone("Europe/London") + void shouldSetAllObjectsWithCorrectSqlType() throws SQLException { + statement = createStatementWithSql( + "INSERT INTO cars(timestamp, date, float, long, big_decimal, null, boolean, int) " + + "VALUES (?,?,?,?,?,?,?,?)"); + + statement.setObject(1, new Timestamp(1564571713000L), Types.TIMESTAMP); + statement.setObject(2, new Date(1564527600000L), Types.DATE); + statement.setObject(3, 5.5F, Types.FLOAT); + statement.setObject(4, 5L, Types.BIGINT); + statement.setObject(5, new BigDecimal("555555555555.55555555"), Types.NUMERIC); + statement.setObject(6, null, Types.JAVA_OBJECT); + statement.setObject(7, true, Types.BOOLEAN); + statement.setObject(8, 5, Types.INTEGER); + + statement.execute(); + + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); assertEquals( "INSERT INTO cars(timestamp, date, float, long, big_decimal, null, boolean, int) VALUES ('2019-07-31 12:15:13','2019-07-31',5.5,5,555555555555.55555555,NULL,1,5)", queryInfoWrapperArgumentCaptor.getValue().getSql()); } + @ParameterizedTest + @CsvSource(value = { + "123," + Types.TINYINT + ",3", + "123," + Types.SMALLINT + ",1", + "123," + Types.INTEGER + ",", + "123," + Types.BIGINT + ",", + }) + // scale is ignored for these types + void shouldSetIntegerObjectWithCorrectSqlType(int value, int type, Integer scale) throws SQLException { + shouldSetObjectWithCorrectSqlType(value, type, scale, String.valueOf(value)); + } + + @ParameterizedTest + @CsvSource(value = { + "3.14," + Types.DECIMAL + ",2,3.14", + "3.1415926," + Types.DECIMAL + ",2,3.14", + "2.7," + Types.NUMERIC + ",2,2.70", + "2.718281828," + Types.NUMERIC + ",1,2.7", + "2.718281828," + Types.NUMERIC + ",5,2.71828", + }) + void shouldSetFloatObjectWithCorrectScalableSqlTypeAndScale(float value, int type, int scale, String expected) throws SQLException { + shouldSetObjectWithCorrectSqlType(value, type, scale, expected); + } + + @ParameterizedTest + @CsvSource(value = { + "3.14," + Types.DECIMAL + ",2,3.14", + "3.1415926," + Types.DECIMAL + ",2,3.14", + "2.7," + Types.NUMERIC + ",2,2.70", + "2.718281828," + Types.NUMERIC + ",1,2.7", + "2.718281828," + Types.NUMERIC + ",5,2.71828", + }) + void shouldSetDoubleObjectWithCorrectScalableSqlTypeAndScale(double value, int type, int scale, String expected) throws SQLException { + shouldSetObjectWithCorrectSqlType(value, type, scale, expected); + } + + @ParameterizedTest + @CsvSource(value = { + "3.14," + Types.FLOAT + ",", + "3.1415926," + Types.DOUBLE + ",", + "3.1415926," + Types.DOUBLE + ",3", // scale is ignored for this type + }) + void shouldSetDoubleObjectWithCorrectSqlTypeAndScale(double value, int type, Integer scale) throws SQLException { + shouldSetObjectWithCorrectSqlType(value, type, scale, Double.toString(value)); + } + + @Test + void unsupportedType() { + statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)"); + assertThrows(SQLException.class, () -> statement.setObject(1, this)); + // STRUCT is not supported now, so it can be used as an example of unsupported type + assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setObject(1, "", Types.STRUCT)); + assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setObject(1, "", Types.STRUCT, 5)); + + // this test definitely cannot be passed to the prepared statement, so exception is expected here. + assertThrows(SQLException.class, () -> statement.setObject(1, this, Types.VARCHAR)); + assertThrows(SQLException.class, () -> statement.setObject(1, this)); + + // unsupported SQL Type + assertThrows(SQLFeatureNotSupportedException.class, () -> statement.setObject(1, "", 999999)); + } + + private void shouldSetObjectWithCorrectSqlType(Object value, int type, Integer scale, String expected) throws SQLException { + statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)"); + if (scale == null) { + statement.setObject(1, value, type); + } else { + statement.setObject(1, value, type, scale); + } + statement.execute(); + verify(fireboltStatementService).execute(queryInfoWrapperArgumentCaptor.capture(), eq(properties), any()); + + assertEquals(format("INSERT INTO data (column) VALUES (%s)", expected), queryInfoWrapperArgumentCaptor.getValue().getSql()); + } + + @Test + void clearParameters() throws SQLException { + statement = createStatementWithSql("INSERT INTO data (column) VALUES (?)"); + statement.setObject(1, ""); // set parameter + statement.execute(); // execute statement - should work because all parameters are set + statement.clearParameters(); // clear parameters; now there are no parameters + assertThrows(IllegalArgumentException.class, () -> statement.execute()); // execution fails because parameters are not set + statement.setObject(1, ""); // set parameter again + statement.execute(); // now execution is successful + } + + @Test + void shouldThrowExceptionWhenExecutedWithSql() throws SQLException { + statement = createStatementWithSql("SELECT 1"); + statement.execute(); // should work + assertThrows(SQLException.class, () -> statement.execute("SELECT 1")); + } + private FireboltPreparedStatement createStatementWithSql(String sql) { - return FireboltPreparedStatement.statementBuilder().statementService(fireboltStatementService).sql(sql) - .sessionProperties(properties).connection(connection).build(); + return new FireboltPreparedStatement(fireboltStatementService, connection, sql); } } diff --git a/src/test/java/com/firebolt/jdbc/type/FireboltDataTypeTest.java b/src/test/java/com/firebolt/jdbc/type/FireboltDataTypeTest.java new file mode 100644 index 00000000..a5c68d9e --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/type/FireboltDataTypeTest.java @@ -0,0 +1,22 @@ +package com.firebolt.jdbc.type; + + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.sql.JDBCType; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class FireboltDataTypeTest { + /** + * This test validates that {@link FireboltDataType#getSqlType()} return standard type defined in {@link java.sql.Types} and {@link JDBCType}. + * @param type - the type + */ + @ParameterizedTest + @EnumSource(FireboltDataType.class) + void sqlType(FireboltDataType type) { + // assert here is just to satisfy static code analysis. valueOf() either succeeds or throws IllegalArgumentException + assertNotNull(JDBCType.valueOf(type.getSqlType())); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLStringTest.java b/src/test/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLStringTest.java index 15792d0d..86c111c1 100644 --- a/src/test/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLStringTest.java +++ b/src/test/java/com/firebolt/jdbc/type/JavaTypeToFireboltSQLStringTest.java @@ -15,28 +15,30 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.Map; +import java.util.TimeZone; import java.util.UUID; import static com.firebolt.jdbc.exception.ExceptionType.TYPE_NOT_SUPPORTED; import static com.firebolt.jdbc.exception.ExceptionType.TYPE_TRANSFORMATION_ERROR; +import static com.firebolt.jdbc.type.JavaTypeToFireboltSQLString.NULL_VALUE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class JavaTypeToFireboltSQLStringTest { @Test - void shouldTransformAnyNullToString() throws FireboltException { + void shouldTransformAnyNullToString() throws SQLException { assertEquals("NULL", JavaTypeToFireboltSQLString.transformAny(null)); } @ParameterizedTest @EnumSource(value = JavaTypeToFireboltSQLString.class) - void shouldTransformNull(JavaTypeToFireboltSQLString type) throws FireboltException { + void shouldTransformNull(JavaTypeToFireboltSQLString type) throws SQLException { assertEquals("NULL", type.transform(null)); } @Test - void shouldTransformBooleanToString() throws FireboltException { + void shouldTransformBooleanToString() throws SQLException { assertEquals("1", JavaTypeToFireboltSQLString.BOOLEAN.transform(true)); assertEquals("0", JavaTypeToFireboltSQLString.BOOLEAN.transform(false)); @@ -45,7 +47,7 @@ void shouldTransformBooleanToString() throws FireboltException { } @Test - void shouldTransformUUIDToString() throws FireboltException { + void shouldTransformUUIDToString() throws SQLException { String uuidValue = "2ac03dc8-f7c9-11ec-b939-0242ac120002"; UUID uuid = UUID.fromString(uuidValue); assertEquals(uuidValue, JavaTypeToFireboltSQLString.UUID.transform(uuid)); @@ -56,7 +58,7 @@ void shouldTransformUUIDToString() throws FireboltException { } @Test - void shouldTransformShortToString() throws FireboltException { + void shouldTransformShortToString() throws SQLException { short s = 123; assertEquals("123", JavaTypeToFireboltSQLString.SHORT.transform(s)); @@ -66,14 +68,14 @@ void shouldTransformShortToString() throws FireboltException { } @Test - void shouldEscapeCharactersWhenTransformingFromString() throws FireboltException { - assertEquals("'105\\' OR 1=1--\\' '", JavaTypeToFireboltSQLString.STRING.transform("105' OR 1=1--' ")); + void shouldEscapeCharactersWhenTransformingFromString() throws SQLException { + assertEquals("'105'' OR 1=1--'' '", JavaTypeToFireboltSQLString.STRING.transform("105' OR 1=1--' ")); - assertEquals("'105\\' OR 1=1--\\' '", JavaTypeToFireboltSQLString.transformAny("105' OR 1=1--' ")); + assertEquals("'105'' OR 1=1--'' '", JavaTypeToFireboltSQLString.transformAny("105' OR 1=1--' ")); } @Test - void shouldTransformLongToString() throws FireboltException { + void shouldTransformLongToString() throws SQLException { assertEquals("105", JavaTypeToFireboltSQLString.LONG.transform(105L)); assertEquals("105", JavaTypeToFireboltSQLString.transformAny(105L)); @@ -82,7 +84,7 @@ void shouldTransformLongToString() throws FireboltException { } @Test - void shouldTransformIntegerToString() throws FireboltException { + void shouldTransformIntegerToString() throws SQLException { assertEquals("105", JavaTypeToFireboltSQLString.INTEGER.transform(105)); assertEquals("105", JavaTypeToFireboltSQLString.transformAny(105)); @@ -91,7 +93,7 @@ void shouldTransformIntegerToString() throws FireboltException { } @Test - void shouldTransformBigIntegerToString() throws FireboltException { + void shouldTransformBigIntegerToString() throws SQLException { assertEquals("1111111111", JavaTypeToFireboltSQLString.BIG_INTEGER.transform(1111111111)); assertEquals("1111111111", JavaTypeToFireboltSQLString.transformAny(1111111111)); @@ -100,7 +102,7 @@ void shouldTransformBigIntegerToString() throws FireboltException { } @Test - void shouldTransformFloatToString() throws FireboltException { + void shouldTransformFloatToString() throws SQLException { assertEquals("1.5", JavaTypeToFireboltSQLString.FLOAT.transform(1.50f)); assertEquals("1.5", JavaTypeToFireboltSQLString.transformAny(1.50f)); @@ -109,8 +111,8 @@ void shouldTransformFloatToString() throws FireboltException { } @Test - void shouldTransformDoubleToString() throws FireboltException { - assertEquals("105", JavaTypeToFireboltSQLString.DOUBLE.transform(105)); + void shouldTransformDoubleToString() throws SQLException { + assertEquals("105.0", JavaTypeToFireboltSQLString.DOUBLE.transform(105)); assertEquals("105", JavaTypeToFireboltSQLString.transformAny(105)); @@ -119,28 +121,35 @@ void shouldTransformDoubleToString() throws FireboltException { @Test @DefaultTimeZone("Europe/London") - void shouldTransformDateToString() throws FireboltException { + void shouldTransformDateToString() throws SQLException { Date d = Date.valueOf(LocalDate.of(2022, 5, 23)); String expectedDateString = "'2022-05-23'"; assertEquals(expectedDateString, JavaTypeToFireboltSQLString.DATE.transform(d)); - assertEquals(expectedDateString, JavaTypeToFireboltSQLString.transformAny((d))); + assertEquals(expectedDateString, JavaTypeToFireboltSQLString.transformAny(d)); } @Test - void shouldTransformTimeToString() throws FireboltException { - assertEquals("105", JavaTypeToFireboltSQLString.DOUBLE.transform(105)); + void shouldTransformDateWithDefaultTimezoneToString() throws SQLException { + assertEquals("'2022-05-23'", JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), TimeZone.getDefault())); + } - assertEquals("105", JavaTypeToFireboltSQLString.transformAny(105)); + @Test + void shouldTransformDateWithDefaultTimezoneIdToString() throws SQLException { + assertEquals("'2022-05-23'", JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), TimeZone.getDefault().getID())); + } - assertEquals("NULL", JavaTypeToFireboltSQLString.DOUBLE.transform(null)); + @Test + void shouldThrowExceptionWhenTransformingDateToStringWithWrongParameter() { + assertEquals(IllegalArgumentException.class, + assertThrows(SQLException.class, () -> JavaTypeToFireboltSQLString.DATE.transform(Date.valueOf(LocalDate.of(2022, 5, 23)), new Object())).getCause().getClass()); } @Test - void shouldTransformTimeStampToString() throws FireboltException { + void shouldTransformTimeStampToString() throws SQLException { Timestamp ts = Timestamp.valueOf(LocalDateTime.of(2022, 5, 23, 12, 57, 13, 173456789)); assertEquals("'2022-05-23 12:57:13.173456789'", JavaTypeToFireboltSQLString.TIMESTAMP.transform(ts)); assertEquals("'2022-05-23 12:57:13.173456789'", JavaTypeToFireboltSQLString.transformAny(ts)); - assertEquals("NULL", JavaTypeToFireboltSQLString.TIMESTAMP.transform(null)); + assertEquals(NULL_VALUE, JavaTypeToFireboltSQLString.TIMESTAMP.transform(null)); } @Test @@ -160,21 +169,31 @@ void shouldTransformArrayOfArray() throws SQLException { } @Test - void shouldTransformJavaArrayOfArray() throws FireboltException { + void shouldTransformJavaArrayOfArray() throws SQLException { String[][] arr = new String[][] { { "a", "b" }, { "c" } }; assertEquals("[['a','b'],['c']]", JavaTypeToFireboltSQLString.ARRAY.transform(arr)); } @Test - void shouldTransformJavaArrayOfPrimitives() throws FireboltException { + void shouldTransformJavaArrayOfPrimitives() throws SQLException { assertEquals("[5]", JavaTypeToFireboltSQLString.ARRAY.transform(new int[] {5})); } @Test - void shouldTransformEmptyArray() throws FireboltException { + void shouldTransformEmptyArray() throws SQLException { assertEquals("[]", JavaTypeToFireboltSQLString.ARRAY.transform(new int[0])); } + @Test + void shouldTransformNullIntSubArray() throws SQLException { + assertEquals("[NULL]", JavaTypeToFireboltSQLString.ARRAY.transform(new int[][] {null})); + } + + @Test + void shouldTransformNullStringSubArray() throws SQLException { + assertEquals("[NULL]", JavaTypeToFireboltSQLString.ARRAY.transform(new String[][] {null})); + } + @Test void shouldThrowExceptionWhenObjectTypeIsNotSupported() { FireboltException ex = assertThrows(FireboltException.class, () -> JavaTypeToFireboltSQLString.transformAny(Map.of())); @@ -186,4 +205,10 @@ void shouldThrowExceptionWhenObjectCouldNotBeTransformed() { FireboltException ex = assertThrows(FireboltException.class, () -> JavaTypeToFireboltSQLString.ARRAY.transform(Map.of())); assertEquals(TYPE_TRANSFORMATION_ERROR, ex.getType()); } + + @ParameterizedTest + @EnumSource(JavaTypeToFireboltSQLString.class) + void shouldTransformNullValue(JavaTypeToFireboltSQLString type) throws SQLException { + assertEquals(NULL_VALUE, type.transform(null)); + } } diff --git a/src/test/java/com/firebolt/jdbc/type/array/SqlArrayUtilTest.java b/src/test/java/com/firebolt/jdbc/type/array/SqlArrayUtilTest.java index cb9cb6db..4bfd4211 100644 --- a/src/test/java/com/firebolt/jdbc/type/array/SqlArrayUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/type/array/SqlArrayUtilTest.java @@ -1,90 +1,272 @@ package com.firebolt.jdbc.type.array; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; +import com.firebolt.jdbc.resultset.column.ColumnType; +import com.firebolt.jdbc.type.FireboltDataType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import java.sql.Array; +import java.sql.JDBCType; import java.sql.SQLException; +import java.util.stream.Stream; -import org.junit.jupiter.api.Test; - -import com.firebolt.jdbc.resultset.column.ColumnType; -import com.firebolt.jdbc.type.FireboltDataType; +import static com.firebolt.jdbc.type.FireboltDataType.BIG_INT; +import static com.firebolt.jdbc.type.FireboltDataType.INTEGER; +import static com.firebolt.jdbc.type.FireboltDataType.TEXT; +import static com.firebolt.jdbc.type.FireboltDataType.TUPLE; +import static java.lang.String.format; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class SqlArrayUtilTest { + private static final String nullableTwoDimIntArray = "Array(Array(int null) null) null"; + private static final String threeDimLongArray = "Array(Array(Array(long)))"; + private static final String textArray = "Array(TEXT)"; + private static final String nullableTwoDimTextArray = "Array(Array(TEXT null) null) null"; + private static final String FB1 = "Firebolt.1"; + private static final String PG = "PostreSQL compliant"; @Test void shouldTransformToEmptyArray() throws SQLException { - String value = "[]"; - FireboltArray emptyArray = new FireboltArray(FireboltDataType.INTEGER, new Integer[] {}); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(INT32)")); + shouldTransformArray("Array(INT32)", "[]", INTEGER, new Integer[] {}); + } - assertEquals(emptyArray.getBaseType(), result.getBaseType()); - assertArrayEquals((Integer[]) emptyArray.getArray(), (Integer[]) result.getArray()); + @ParameterizedTest + @CsvSource(value = { + // old format + "Array(INT32);[1,2,3,\\N,5]", + "Array(int);[1,2,3,\\N,5]", + "Array(int null);[1,2,3,\\N,5]", + "Array(int) null;[1,2,3,\\N,5]", + "Array(int null) null;[1,2,3,\\N,5]", + // new/v2/postgres compliant + "Array(INT32);{1,2,3,\\N,5}", + "Array(int);{1,2,3,\\N,5}", + "Array(int null);{1,2,3,\\N,5}", + "Array(int) null;{1,2,3,\\N,5}", + "Array(int null) null;{1,2,3,\\N,5}" + }, + delimiter = ';') + void shouldTransformIntArray(String type, String value) throws SQLException { + shouldTransformArray(type, value, INTEGER, new Integer[] { 1, 2, 3, null, 5 }); } - @Test - void shouldTransformIntArray() throws SQLException { - String value = "[1,2,3,\\N,5]"; - FireboltArray expectedArray = new FireboltArray(FireboltDataType.INTEGER, new Integer[] { 1, 2, 3, null, 5 }); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(INT32)")); + @ParameterizedTest + @ValueSource(strings = { + "['1','2','3','',\\N,'5']", + "['1','2','3','',NULL,'5']", + "{1,2,3,\"\",\\N,5}", + "{1,2,3,\"\",NULL,5}", + }) + void shouldTransformStringArray(String value) throws SQLException { + shouldTransformArray("Array(TEXT)", value, TEXT, new String[] { "1", "2", "3", "", null, "5" }); + } - assertEquals(expectedArray.getBaseType(), result.getBaseType()); - assertArrayEquals((Integer[]) expectedArray.getArray(), (Integer[]) result.getArray()); + @ParameterizedTest + @ValueSource(strings = { + "[' a','b ',' c ',' ','a b',' c d ']", + "{\" a\",\"b \",\" c \",\" \",\"a b\",\" c d \"}" + }) + void shouldTransformStringArrayWithSpaces(String value) throws SQLException { + shouldTransformArray("Array(TEXT)", value, TEXT, new String[] { " a", "b ", " c ", " ", "a b", " c d " }); } - @Test - void shouldTransformStringArray() throws SQLException { - String value = "['1','2','3','',\\N,'5']"; - FireboltArray expectedArray = new FireboltArray(FireboltDataType.TEXT, new String[] { "1", "2", "3", "", null, "5" }); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(TEXT)")); + @ParameterizedTest + @ValueSource(strings = { + "['1','2,','3','',\\N,'5']", + "{1,\"2,\",3,\"\",\\N,5}" + }) + void shouldTransformStringArrayWithComma(String value) throws SQLException { + shouldTransformArray("Array(TEXT)", value, TEXT, new String[] { "1", "2,", "3", "", null, "5" }); + } - assertEquals(expectedArray.getBaseType(), result.getBaseType()); - assertArrayEquals((String[]) expectedArray.getArray(), (String[]) result.getArray()); + @ParameterizedTest + @ValueSource(strings = { + "[(1,'a'),(2,'b'),(3,'c')]", + "{\"('1','a')\",\"('2','b')\",\"('3','c')\"}" + }) + void shouldTransformArrayOfTuples(String value) throws SQLException { + Object[][] expectedArray = new Object[][] { { 1, "a" }, { 2, "b" }, { 3, "c" } }; + shouldTransformArray("Array(TUPLE(int,string))", value, TUPLE, expectedArray); } - @Test - void shouldTransformStringArrayWithComma() throws SQLException { - String value = "['1','2,','3','',\\N,'5']"; - FireboltArray expectedArray = new FireboltArray(FireboltDataType.TEXT, new String[] { "1", "2,", "3", "", null, "5" }); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(TEXT)")); + @ParameterizedTest + @ValueSource(strings = { + "[[(1,'(a))'),(2,'[b]'),(3,'[]c[')],[(4,'d')]]", + "{{\"(1,'(a))')\",\"(2,'[b]')\",\"(3,'[]c[')\"},{\"(4,'d')\"}}" + }) + void shouldTransformArrayOfArrayTuples(String value) throws SQLException { + Object[][][] expectedArray = new Object[][][] { + { { 1, "(a))" }, { 2, "[b]" }, { 3, "[]c[" } }, + { { 4, "d" } } + }; + shouldTransformArray("Array(Array(TUPLE(int,string)))", value, TUPLE, expectedArray); + } - assertEquals(expectedArray.getBaseType(), result.getBaseType()); - assertArrayEquals((String[]) expectedArray.getArray(), (String[]) result.getArray()); + @ParameterizedTest + @ValueSource(strings = { + "[(1,'a','1a'),(2,'b','2b'),(3,'[c]','3c')]", + "{\"(1,'a','1a')\",\"(2,'b','2b')\",\"(3,'[c]','3c')\"}" + }) + void shouldTransformArrayOfTuplesWithSpecialCharacters(String value) throws SQLException { + Object[][] expectedArray = new Object[][] { { 1, "a", "1a" }, { 2, "b", "2b" }, { 3, "[c]", "3c" } }; + shouldTransformArray("Array(TUPLE(int,string,string))", value, TUPLE, expectedArray); } - @Test - void shouldTransformArrayOfTuples() throws SQLException { - String value = "[(1,'a'),(2,'b'),(3,'c')]"; - Object[][] expectedArray = new Object[][] { { 1, "a" }, { 2, "b" }, { 3, "c" } }; - FireboltArray expectedFireboltArray = new FireboltArray(FireboltDataType.TUPLE, expectedArray); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(TUPLE(int,string))")); + @ParameterizedTest + @ValueSource(strings = {"Array(Array(int))", "Array(Array(int null))", "Array(Array(int null) null)", "Array(Array(int null) null) null"}) + void shouldTransformBiDimensionalEmptyIntArray(String type) throws SQLException { + shouldTransformArray(type, "[[]]", INTEGER, new Integer[][]{new Integer[]{}}); + } + + @ParameterizedTest + @ValueSource(strings = {"Array(Array(int))", "Array(Array(int null))", "Array(Array(int null) null)", "Array(Array(int null) null) null"}) + void shouldTransformBiDimensionalIntArrayOneElement(String type) throws SQLException { + shouldTransformArray(type, "[[1,2,3]]", INTEGER, new Integer[][]{{1, 2, 3}}); + } + + @ParameterizedTest + @ValueSource(strings = {"Array(double) null", "Array(Array(int null) null) null"}) + void shouldTransformNullArrayToNull(String type) throws SQLException { + assertNull(SqlArrayUtil.transformToSqlArray("NULL", ColumnType.of(type))); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void nullByteArrayToString(boolean separateEachByte) { + assertNull(SqlArrayUtil.byteArrayToHexString(null, separateEachByte)); + } + + @ParameterizedTest + @CsvSource(value = { + "ABC;false;\\x414243", + "abc;true;\\x61\\x62\\x63", + "Hello, world!;false;\\x48656c6c6f2c20776f726c6421", + "Hello, world!;true;\\x48\\x65\\x6c\\x6c\\x6f\\x2c\\x20\\x77\\x6f\\x72\\x6c\\x64\\x21", + "6/3=2;false;\\x362f333d32", + "6/3=2;true;\\x36\\x2f\\x33\\x3d\\x32", + "x\\y;false;\\x785c79", + "x\\y;true;\\x78\\x5c\\x79" + }, delimiter = ';') + void byteArrayToHexString(String str, boolean separateEachByte, String expectedHex) { + assertEquals(expectedHex, SqlArrayUtil.byteArrayToHexString(str.getBytes(), separateEachByte)); + } - assertEquals(expectedFireboltArray.getBaseType(), result.getBaseType()); - assertArrayEquals(expectedArray, (Object[]) result.getArray()); + @ParameterizedTest + @CsvSource(value = { + "false;\\x4d756c74690a6c696e650a74657874", + "true;\\x4d\\x75\\x6c\\x74\\x69\\x0a\\x6c\\x69\\x6e\\x65\\x0a\\x74\\x65\\x78\\x74" + }, delimiter = ';') + void byteArrayWithNewLineToHexString(boolean separateEachByte, String expectedHex) { + byteArrayToHexString("Multi\nline\ntext", separateEachByte, expectedHex); } @Test - void shouldTransformArrayOfArrayTuples() throws SQLException { - String value = "[[(1,'(a))'),(2,'[b]'),(3,'[]c[')],[(4,'d')]]"; - Object[][][] expectedArray = new Object[][][] { { { 1, "(a))" }, { 2, "[b]" }, { 3, "[]c[" } }, - { { 4, "d" } } }; - FireboltArray expectedFireboltArray = new FireboltArray(FireboltDataType.TUPLE, expectedArray); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(Array(TUPLE(int,string)))")); + void nullHexStringToByteArray() { + assertNull(SqlArrayUtil.hexStringToByteArray(null)); + } - assertEquals(expectedFireboltArray.getBaseType(), result.getBaseType()); - assertArrayEquals(expectedArray, (Object[][]) result.getArray()); + @ParameterizedTest + @CsvSource({ + "\\x78797A,xyz", + "\\x4a4B4c,JKL", + "\\x20,' '", + "\\x30,0", + "\\x2A,*", + "\\x2F,/", + "hello,hello" // not hex string + }) + void hexStringToByteArray(String hex, String expected) { + assertArrayEquals(expected.getBytes(), SqlArrayUtil.hexStringToByteArray(hex)); } @Test - void shouldTransformArrayOfTuplesWithSpecialCharacters() throws SQLException { - String value = "[(1,'a','1a'),(2,'b','2b'),(3,'[c]','3c')]"; - Object[][] expectedArray = new Object[][] { { 1, "a", "1a" }, { 2, "b", "2b" }, { 3, "[c]", "3c" } }; - FireboltArray expectedFireboltArray = new FireboltArray(FireboltDataType.TUPLE, expectedArray); - Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of("Array(TUPLE(int,string,string))")); + void notHexStringToByteArray() { + assertArrayEquals("nothing".getBytes(), SqlArrayUtil.hexStringToByteArray("nothing")); + } + + @ParameterizedTest + @ValueSource(strings = { + "ABC", "abc", "Hello, world!", "Multi\nlinentext" + }) + @NullSource + void byteArrayToHexStringAndBack(String str) { + byte[] bytes = SqlArrayUtil.hexStringToByteArray(SqlArrayUtil.byteArrayToHexString(str == null ? null : str.getBytes(), false)); + assertEquals(str, bytes == null ? null : new String(bytes)); + } + + @ParameterizedTest + @CsvSource({"\\x2G,G", "\\xH0,H"}) + void wrongHexStringToByteArray(String hex, String expectedWrongCharacter) { + @SuppressWarnings("java:S5778") // "Refactor the code of the lambda to have only one invocation possibly throwing a runtime exception" - this is the purpose of this test + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new String(SqlArrayUtil.hexStringToByteArray(hex))); + assertEquals(format("Illegal character %s in hex string", expectedWrongCharacter), e.getMessage()); + } + + private static Stream biDimensionalIntArray() { + return Stream.of( + // 2 dim integer + Arguments.of(FB1, nullableTwoDimIntArray, "[[1,2],[3]]", INTEGER, new Integer[][] {{1,2}, {3}}), + Arguments.of(FB1, nullableTwoDimIntArray, "[[4,NULL,5]]", INTEGER, new Integer[][] {{4, null, 5}}), + Arguments.of(FB1, nullableTwoDimIntArray, "[NULL,[4],NULL]", INTEGER, new Integer[][] {null, {4}, null}), + Arguments.of(FB1, nullableTwoDimIntArray, "[[4],NULL,[5,NULL,6]]", INTEGER, new Integer[][] {{4}, null, {5,null,6}}), + Arguments.of(FB1, nullableTwoDimIntArray, "[[NULL,7]]", INTEGER, new Integer[][] {{null,7}}), + // 3 dim integer + Arguments.of(FB1, threeDimLongArray, "[[[1,2,3]]]", BIG_INT, new Long[][][] {{{1L, 2L, 3L}}}), + Arguments.of(FB1, threeDimLongArray, "[[[10]],[[1],[2,3],[4,5]],[[20]]]", BIG_INT, new Long[][][] {{{10L}}, {{1L}, {2L, 3L}, {4L, 5L}}, {{20L}}}), + Arguments.of(FB1, threeDimLongArray, "[NULL,[NULL,[1,2,3],NULL],NULL]", BIG_INT, new Long[][][] {null, {null, {1L, 2L, 3L}, null}, null}), + Arguments.of(FB1, threeDimLongArray, "[[[]]]", BIG_INT, new Long[][][] {{{}}}), + // text array + Arguments.of(FB1, textArray, "['Hello','Bye']", TEXT, new String[] {"Hello", "Bye"}), + Arguments.of(FB1, textArray, "['What\\'s up','ok','a[1]']", TEXT, new String[] {"What's up", "ok", "a[1]"}), + Arguments.of(FB1, nullableTwoDimTextArray, "[['one','two'],['three']]", TEXT, new String[][] {{"one", "two"}, {"three"}}) + ); + } + + private static Stream biDimensionalIntArrayPostgresCompliant() { + return Stream.of( + // 2 dim integer + Arguments.of(PG, nullableTwoDimIntArray, "{{1,2},{3}}", INTEGER, new Integer[][] {{1,2}, {3}}), + Arguments.of(PG, nullableTwoDimIntArray, "{{4,NULL,5}}", INTEGER, new Integer[][] {{4, null, 5}}), + Arguments.of(PG, nullableTwoDimIntArray, "{NULL,{4},NULL}", INTEGER, new Integer[][] {null, {4}, null}), + Arguments.of(PG, nullableTwoDimIntArray, "{{4},NULL,{5,NULL,6}}", INTEGER, new Integer[][] {{4}, null, {5,null,6}}), + Arguments.of(PG, nullableTwoDimIntArray, "{{NULL,7}}", INTEGER, new Integer[][] {{null,7}}), + // 3 dim integer + Arguments.of(PG, threeDimLongArray, "{{{1,2,3}}}", BIG_INT, new Long[][][] {{{1L, 2L, 3L}}}), + Arguments.of(PG, threeDimLongArray, "{{{10}},{{1},{2,3},{4,5}},{{20}}}", BIG_INT, new Long[][][] {{{10L}}, {{1L}, {2L, 3L}, {4L, 5L}}, {{20L}}}), + Arguments.of(PG, threeDimLongArray, "{NULL,{NULL,{1,2,3},NULL},NULL}", BIG_INT, new Long[][][] {null, {null, {1L, 2L, 3L}, null}, null}), + Arguments.of(PG, threeDimLongArray, "{{{}}}", BIG_INT, new Long[][][] {{{}}}), + // text array + Arguments.of(PG, textArray, "{Hello,Bye}", TEXT, new String[] {"Hello", "Bye"}), + Arguments.of(PG, textArray, "{\"What's up\",ok,a[1]}", TEXT, new String[] {"What's up", "ok", "a[1]"}), + Arguments.of(PG, nullableTwoDimTextArray, "{{one,two},{three}}", TEXT, new String[][] {{"one", "two"}, {"three"}}) + ); + } + + @ParameterizedTest(name = "{0}:{1}:{2}") + @MethodSource({"biDimensionalIntArray", "biDimensionalIntArrayPostgresCompliant"}) + void shouldTransformBiDimensionalIntArraySeveralElements(String format, String type, String input, FireboltDataType expectedType, Object[] expected) throws SQLException { + shouldTransformArray(type, input, expectedType, expected); + } + + + void shouldTransformArray(String typeDef, String value, FireboltDataType expectedType, Object expectedValue) throws SQLException { + FireboltArray expectedArray = new FireboltArray(expectedType, expectedValue); + Array result = SqlArrayUtil.transformToSqlArray(value, ColumnType.of(typeDef)); + + assertJdbcType(expectedArray.getBaseType(), result.getBaseType()); + assertArrayEquals((Object[])expectedArray.getArray(), (Object[])result.getArray()); + } - assertEquals(expectedFireboltArray.getBaseType(), result.getBaseType()); - assertArrayEquals(expectedArray, (Object[]) result.getArray()); + private void assertJdbcType(int expected, int actual) { + assertEquals(expected, actual, () -> format("Wrong type: expected %s(%d) but was %s(%d)", JDBCType.valueOf(expected), expected, JDBCType.valueOf(actual), actual)); } } diff --git a/src/test/java/com/firebolt/jdbc/type/lob/FireboltBlobTest.java b/src/test/java/com/firebolt/jdbc/type/lob/FireboltBlobTest.java new file mode 100644 index 00000000..fbfdd3e3 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/type/lob/FireboltBlobTest.java @@ -0,0 +1,192 @@ +package com.firebolt.jdbc.type.lob; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.io.OutputStream; +import java.sql.Blob; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FireboltBlobTest extends FireboltLobTest { + @Test + void empty() throws SQLException, IOException { + Blob blob = new FireboltBlob(); + assertEquals(0, blob.length()); + + assertEquals(0, blob.getBytes(1, 0).length); + assertThrows(SQLException.class, () -> blob.getBytes(1, 1)); + assertThrows(SQLException.class, () -> blob.getBytes(1, 123)); + assertThrows(SQLException.class, () -> blob.getBytes(0, 123)); + assertThrows(SQLException.class, () -> blob.getBytes(0, 1)); + assertThrows(SQLException.class, () -> blob.getBytes(1, -1)); + + assertEquals(-1, blob.getBinaryStream(1, 0).read()); + assertThrows(SQLException.class, () -> blob.getBinaryStream(1, 1)); + assertThrows(SQLException.class, () -> blob.getBinaryStream(1, 123)); + assertThrows(SQLException.class, () -> blob.getBinaryStream(1, -1)); + + assertEquals(-1, blob.getBinaryStream().read()); + assertArrayEquals(new byte[0], blob.getBinaryStream().readAllBytes()); + } + + @Test + void preInitialized() throws SQLException, IOException { + String str = "hello, world!"; + Blob blob = new FireboltBlob(str.getBytes()); + + assertEquals(str.length(), blob.length()); + + assertEquals("", new String(blob.getBytes(1, 0))); + assertEquals("h", new String(blob.getBytes(1, 1))); + assertEquals(str, new String(blob.getBytes(1, str.length()))); + assertEquals("hello", new String(blob.getBytes(1, 5))); + assertEquals("world", new String(blob.getBytes(8, 5))); + assertEquals("world!", new String(blob.getBytes(8, 6))); + assertThrows(SQLException.class, () -> blob.getBinaryStream(1, str.length() + 1)); + assertThrows(SQLException.class, () -> blob.getBinaryStream(0, str.length())); + assertThrows(SQLException.class, () -> blob.getBinaryStream(0, str.length() + 1)); + + assertEquals(str, new String(blob.getBinaryStream().readAllBytes())); + } + + @Test + void binaryStreamToEmptyBlob() throws SQLException, IOException { + String str = "hello, world!"; + Blob blob = new FireboltBlob(); + try (OutputStream os = blob.setBinaryStream(1)) { + os.write(str.getBytes()); + } + assertEquals(str, new String(blob.getBinaryStream().readAllBytes())); + } + + @Test + void characterStreamWithFlush() throws SQLException, IOException { + String str = "hello, world!"; + Blob blob = new FireboltBlob(str.getBytes()); + try (OutputStream os = blob.setBinaryStream(8)) { + os.write("all".getBytes()); + assertEquals(str, new String(blob.getBinaryStream().readAllBytes())); + os.flush(); + assertEquals("hello, allld!", new String(blob.getBinaryStream().readAllBytes())); + os.write(" ".getBytes()); + os.write("people".getBytes()); + os.write("!".getBytes()); + assertEquals("hello, allld!", new String(blob.getBinaryStream().readAllBytes())); + } + // the rest is flushed automatically when writer is closed + assertEquals("hello, all people!", new String(blob.getBinaryStream().readAllBytes())); + } + + @Test + void failedToWriteToClosedWriter() throws SQLException, IOException { + Blob blob = new FireboltBlob(); + OutputStream os = blob.setBinaryStream(1); + os.close(); + assertThrows(IOException.class, () -> os.write(1)); + } + + @ParameterizedTest + @MethodSource("replace") + void binaryStreamReplace(String initial, String replacement, int pos, String expected) throws SQLException, IOException { + Blob blob = new FireboltBlob(initial.getBytes()); + try (OutputStream os = blob.setBinaryStream(pos)) { + os.write(replacement.getBytes()); + } + assertEquals(expected, new String(blob.getBinaryStream().readAllBytes())); + } + + @Test + void setStringToEmptyBlob() throws SQLException { + String str = "hello, world!"; + Blob blob = new FireboltBlob(); + blob.setBytes(1, str.getBytes()); + assertEquals(str, new String(blob.getBytes(1, str.length()))); + } + + @ParameterizedTest + @MethodSource("replace") + void stringReplace(String initial, String replacement, int pos, String expected) throws SQLException { + Blob blob = new FireboltBlob(initial.getBytes()); + assertEquals(initial, new String(blob.getBytes(1, initial.length()))); + blob.setBytes(pos, replacement.getBytes()); + assertEquals(expected, new String(blob.getBytes(1, expected.length()))); + } + + @ParameterizedTest + @MethodSource("partialReplace") + void partialStringReplace(String initial, String replacement, int pos, int offset, int length, String expected) throws SQLException { + Blob blob = new FireboltBlob(initial.getBytes()); + assertEquals(initial, new String(blob.getBytes(1, initial.length()))); + blob.setBytes(pos, replacement.getBytes(), offset, length); + assertEquals(expected, new String(blob.getBytes(1, expected.length()))); + } + + @ParameterizedTest + @MethodSource("wrongReplace") + void wrongReplace(String initial, String replacement, int pos, int offset, int length) throws SQLException { + Blob blob = new FireboltBlob(initial.getBytes()); + assertEquals(initial, new String(blob.getBytes(1, initial.length()))); + assertThrows(SQLException.class, () -> blob.setBytes(pos, replacement.getBytes(), offset, length)); + } + + @ParameterizedTest + @MethodSource("truncate") + void truncate(String str, int length, String expected) throws SQLException, IOException { + Blob blob = new FireboltBlob(str.getBytes()); + blob.truncate(length); + assertEquals(expected, new String(blob.getBinaryStream().readAllBytes())); + } + + @ParameterizedTest + @ValueSource(longs = {14L, -1L}) + void truncateWrongNumber(long length) { + String str = "hello, world!"; + Blob blob = new FireboltBlob(str.getBytes()); + assertThrows(SQLException.class, () -> blob.truncate(length)); + } + + @Test + void invalid() throws SQLException, IOException { + Blob blob = new FireboltBlob("".getBytes()); + assertEquals("", new String(blob.getBinaryStream().readAllBytes())); + blob.free(); + assertThrows(SQLException.class, blob::getBinaryStream); + } + + @ParameterizedTest + @MethodSource("position") + void position(String str, String search, int start, int expected) throws SQLException { + Blob blob = new FireboltBlob(str.getBytes()); + assertEquals(expected, blob.position(search.getBytes(), start)); + assertEquals(expected, blob.position(new FireboltBlob(search.getBytes()), start)); + } + + @ParameterizedTest + @MethodSource("equalsAndHashCode") + void equalsAndHashCode(String s1, String s2, boolean expectedEquals) { + Blob blob1 = new FireboltBlob(s1.getBytes()); + Blob blob2 = new FireboltBlob(s2.getBytes()); + if (expectedEquals) { + assertEquals(blob1, blob2); + assertEquals(blob1.hashCode(), blob2.hashCode()); + } else { + assertNotEquals(blob1, blob2); + assertNotEquals(blob1.hashCode(), blob2.hashCode()); + } + } + + @Test + void equalsNotCompatibleObject() { + String text = "hello"; + Blob blob = new FireboltBlob(text.getBytes()); + assertNotEquals(blob, text); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/type/lob/FireboltClobTest.java b/src/test/java/com/firebolt/jdbc/type/lob/FireboltClobTest.java new file mode 100644 index 00000000..38f50907 --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/type/lob/FireboltClobTest.java @@ -0,0 +1,211 @@ +package com.firebolt.jdbc.type.lob; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.sql.Clob; +import java.sql.SQLException; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FireboltClobTest extends FireboltLobTest { + @Test + void empty() throws SQLException, IOException { + Clob clob = new FireboltClob(); + assertEquals(0, clob.length()); + + assertEquals(0, clob.getSubString(1, 0).length()); + assertThrows(SQLException.class, () -> clob.getSubString(1, 1)); + assertThrows(SQLException.class, () -> clob.getSubString(1, 123)); + assertThrows(SQLException.class, () -> clob.getSubString(0, 123)); + assertThrows(SQLException.class, () -> clob.getSubString(0, 1)); + assertThrows(SQLException.class, () -> clob.getSubString(1, -1)); + + assertEquals(-1, clob.getCharacterStream(1, 0).read()); + assertThrows(SQLException.class, () -> clob.getCharacterStream(1, 1)); + assertThrows(SQLException.class, () -> clob.getCharacterStream(1, 123)); + assertThrows(SQLException.class, () -> clob.getCharacterStream(1, -1)); + + assertEquals(-1, clob.getAsciiStream().read()); + assertEquals("", readAll(clob.getCharacterStream())); + } + + @Test + void preInitialized() throws SQLException, IOException { + String str = "hello, world!"; + Clob clob = new FireboltClob(str.toCharArray()); + + assertEquals(str.length(), clob.length()); + + assertEquals("", clob.getSubString(1, 0)); + assertEquals("h", clob.getSubString(1, 1)); + assertEquals(str, clob.getSubString(1, str.length())); + assertEquals("hello", clob.getSubString(1, 5)); + assertEquals("world", clob.getSubString(8, 5)); + assertEquals("world!", clob.getSubString(8, 6)); + assertThrows(SQLException.class, () -> clob.getCharacterStream(1, str.length() + 1)); + assertThrows(SQLException.class, () -> clob.getCharacterStream(0, str.length())); + assertThrows(SQLException.class, () -> clob.getCharacterStream(0, str.length() + 1)); + + assertEquals(str, new String(clob.getAsciiStream().readAllBytes())); + + assertEquals(str, readAll(clob.getCharacterStream())); + } + + @Test + void asciiStream() throws SQLException, IOException { + String str = "hello, world!"; + Clob clob = new FireboltClob(); + try (OutputStream os = clob.setAsciiStream(1)) { + os.write(str.getBytes()); + } + assertEquals(str, new String(clob.getAsciiStream().readAllBytes())); + } + + @Test + void characterStream() throws SQLException, IOException { + String str = "hello, world!"; + Clob clob = new FireboltClob(); + try (Writer writer = clob.setCharacterStream(1)) { + writer.write(str); + } + assertEquals(str, readAll(clob.getCharacterStream())); + } + + @Test + void characterStreamWithFlush() throws SQLException, IOException { + String str = "hello, world!"; + Clob clob = new FireboltClob(str.toCharArray()); + try (Writer writer = clob.setCharacterStream(8)) { + writer.write("all"); + assertEquals(str, readAll(clob.getCharacterStream())); + writer.flush(); + assertEquals("hello, allld!", readAll(clob.getCharacterStream())); + writer.write(" "); + writer.write("people"); + writer.write("!"); + assertEquals("hello, allld!", readAll(clob.getCharacterStream())); + } + // the rest is flushed automatically when writer is closed + assertEquals("hello, all people!", readAll(clob.getCharacterStream())); + } + + @Test + void failedToWriteToClosedWriter() throws SQLException, IOException { + Clob clob = new FireboltClob(); + Writer writer = clob.setCharacterStream(1); + writer.close(); + assertThrows(IOException.class, () -> writer.write("x")); + } + + @ParameterizedTest + @MethodSource("replace") + void binaryStreamReplace(String initial, String replacement, int pos, String expected) throws SQLException, IOException { + Clob clob = new FireboltClob(initial.toCharArray()); + try (Writer writer = clob.setCharacterStream(pos)) { + writer.write(replacement); + } + assertEquals(expected, readAll(clob.getCharacterStream())); + } + + @ParameterizedTest + @MethodSource("replace") + void stringReplaceCharacters(String initial, String replacement, int pos, String expected) throws SQLException { + Clob clob = new FireboltClob(initial.toCharArray()); + assertEquals(initial, clob.getSubString(1, initial.length())); + clob.setString(pos, replacement); + assertEquals(expected, clob.getSubString(1, expected.length())); + } + + @ParameterizedTest + @MethodSource("partialReplace") + void partialStringReplace(String initial, String replacement, int pos, int offset, int length, String expected) throws SQLException { + Clob clob = new FireboltClob(initial.toCharArray()); + assertEquals(initial, clob.getSubString(1, initial.length())); + clob.setString(pos, replacement, offset, length); + assertEquals(expected, clob.getSubString(1, expected.length())); + } + + @ParameterizedTest + @MethodSource("wrongReplace") + void wrongReplace(String initial, String replacement, int pos, int offset, int length) throws SQLException { + Clob clob = new FireboltClob(initial.toCharArray()); + assertEquals(initial, clob.getSubString(1, initial.length())); + assertThrows(SQLException.class, () -> clob.setString(pos, replacement, offset, length)); + } + + @Test + void setStringToEmptyClob() throws SQLException { + String str = "hello, world!"; + Clob clob = new FireboltClob(); + clob.setString(1, str); + assertEquals(str, clob.getSubString(1, str.length())); + } + + @ParameterizedTest + @MethodSource("truncate") + void truncate(String str, int length, String expected) throws SQLException, IOException { + Clob clob = new FireboltClob(str.toCharArray()); + clob.truncate(length); + assertEquals(expected, new String(clob.getAsciiStream().readAllBytes())); + } + + @ParameterizedTest + @ValueSource(longs = {14L, -1L}) + void truncateWrongNumber(long length) { + String str = "hello, world!"; + Clob clob = new FireboltClob(str.toCharArray()); + assertThrows(SQLException.class, () -> clob.truncate(length)); + } + + @Test + void invalid() throws SQLException, IOException { + Clob clob = new FireboltClob("".toCharArray()); + assertEquals("", new String(clob.getAsciiStream().readAllBytes())); + clob.free(); + assertThrows(SQLException.class, clob::getAsciiStream); + } + + @ParameterizedTest + @MethodSource("position") + void position(String str, String search, int start, int expected) throws SQLException { + Clob clob = new FireboltClob(str.toCharArray()); + assertEquals(expected, clob.position(search, start)); + assertEquals(expected, clob.position(new FireboltClob(search.toCharArray()), start)); + } + + @ParameterizedTest + @MethodSource("equalsAndHashCode") + void equalsAndHashCode(String s1, String s2, boolean expectedEquals) { + Clob clob1 = new FireboltClob(s1.toCharArray()); + Clob clob2 = new FireboltClob(s2.toCharArray()); + if (expectedEquals) { + assertEquals(clob1, clob2); + assertEquals(clob1.hashCode(), clob2.hashCode()); + } else { + assertNotEquals(clob1, clob2); + assertNotEquals(clob1.hashCode(), clob2.hashCode()); + } + } + + @Test + void equalsNotCompatibleObject() { + String text = "hello"; + Clob clob = new FireboltClob(text.toCharArray()); + assertNotEquals(clob, text); + } + + private String readAll(Reader reader) { + return new BufferedReader(reader).lines().collect(Collectors.joining(System.lineSeparator())); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/type/lob/FireboltLobTest.java b/src/test/java/com/firebolt/jdbc/type/lob/FireboltLobTest.java new file mode 100644 index 00000000..27b8a0ad --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/type/lob/FireboltLobTest.java @@ -0,0 +1,79 @@ +package com.firebolt.jdbc.type.lob; + +import org.junit.jupiter.params.provider.Arguments; + +import java.util.stream.Stream; + +abstract class FireboltLobTest { + protected static Stream replace() { + return Stream.of( + Arguments.of("hey, world!", "bye", 1, "bye, world!"), + Arguments.of("hello, all!", "world!", 8, "hello, world!"), + Arguments.of("hello, world!", "hello, world?", 1, "hello, world?"), + Arguments.of("hello, world!", "!", 6, "hello! world!"), + Arguments.of("hello, world!", "", 5, "hello, world!") + ); + } + + protected static Stream partialReplace() { + return Stream.of( + Arguments.of("hey, world!", "bye", 1, 0, 3, "bye, world!"), + Arguments.of("hello, all!", "world!", 8, 0, 6, "hello, world!"), + Arguments.of("hello, world!", "hello, world?", 1, 0, 13, "hello, world?"), + Arguments.of("hello, world!", "!", 6, 0, 1, "hello! world!"), + Arguments.of("hello, world!", "", 5, 0, 0, "hello, world!"), + + Arguments.of("hello, world!", "world!", 8, 0, 5, "hello, world"), + Arguments.of("hello, world!", "all people", 8, 4, 6, "hello, people"), + Arguments.of("hello, all!", "bye people", 7, 3, 7, "hello, people") + ); + } + + protected static Stream wrongReplace() { + return Stream.of( + Arguments.of("hello, all!", "people", -1, 0, 6), + Arguments.of("hello, all!", "people", 1, -1, 6), + Arguments.of("hello, all!", "people", 1, 0, 7), + Arguments.of("hello, all!", "people", 1, 7, 0), + Arguments.of("hello, all!", "people", 1, 5, 2) + ); + } + + protected static Stream truncate() { + return Stream.of( + Arguments.of("hello, world!", 5, "hello"), + Arguments.of("hello, world!", 13, "hello, world!"), + Arguments.of("hello, world!", 0, ""), + Arguments.of("hello, world!", 1, "h") + ); + } + + protected static Stream position() { + return Stream.of( + Arguments.of("hello, world!", "hello", 1, 1), + Arguments.of("hello, world!", "world", 1, 8), + Arguments.of("hello, world!", "world", 3, 8), + Arguments.of("hello, world!", "world", 8, 8), + Arguments.of("hello, world!", "world", 9, -1), + Arguments.of("hello, world!", "world", 0, -1), + Arguments.of("hello, world!", "world", 14, -1), + Arguments.of("hello, world!", "bye", 1, -1), + Arguments.of("hello, world!", "hello", 5, -1), + Arguments.of("1212", "112", 1, -1), + Arguments.of("", "", 1, -1), + Arguments.of("hello", "h", 0, -1), + Arguments.of("hello", "", 1, 1) + ); + } + + protected static Stream equalsAndHashCode() { + return Stream.of( + Arguments.of("one", "one", true), + Arguments.of("one", "two", false), + Arguments.of("two", "one", false), + Arguments.of("", "", true), + Arguments.of("something", "", false), + Arguments.of("", "anything", false) + ); + } +} diff --git a/src/test/java/com/firebolt/jdbc/util/InputStreamUtilTest.java b/src/test/java/com/firebolt/jdbc/util/InputStreamUtilTest.java index c4f1b782..aa742bb4 100644 --- a/src/test/java/com/firebolt/jdbc/util/InputStreamUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/util/InputStreamUtilTest.java @@ -1,10 +1,13 @@ package com.firebolt.jdbc.util; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.StringReader; import static org.junit.jupiter.api.Assertions.*; @@ -22,4 +25,17 @@ void shouldReadAllBytes() throws IOException { void shouldNotThrowExceptionIfStreamIsNull() { assertDoesNotThrow(() -> InputStreamUtil.readAllBytes(null)); } + + @ParameterizedTest + @CsvSource(value = { + "hello,5,hello", + "hello,6,hello", + "hello,42,hello", + "hello,4,hell", + "hello,1,h", + "hello,0,''", + }) + void read(String in, int length, String expected) throws IOException { + assertEquals(expected, InputStreamUtil.read(new StringReader(in), length)); + } } \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/util/LoggerUtilTest.java b/src/test/java/com/firebolt/jdbc/util/LoggerUtilTest.java index 7b334070..245198ee 100644 --- a/src/test/java/com/firebolt/jdbc/util/LoggerUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/util/LoggerUtilTest.java @@ -1,23 +1,23 @@ package com.firebolt.jdbc.util; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; - -import com.firebolt.jdbc.log.FireboltLogger; -import com.firebolt.jdbc.log.SLF4JLogger; +import org.slf4j.bridge.SLF4JBridgeHandler; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.Arrays; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; class LoggerUtilTest { @Test void shouldGetSLF4JLoggerWhenAvailable() { - FireboltLogger fireboltLogger = LoggerUtil.getLogger("myLogger"); + Logger fireboltLogger = LoggerUtil.getRootLogger(); // Should be true since SLF4J is available - assertTrue(fireboltLogger instanceof SLF4JLogger); + assertTrue(Arrays.stream(fireboltLogger.getHandlers()).anyMatch(handler -> handler instanceof SLF4JBridgeHandler)); } @Test diff --git a/src/test/java/com/firebolt/jdbc/util/PropertyUtilTest.java b/src/test/java/com/firebolt/jdbc/util/PropertyUtilTest.java index bc2aadfa..e64869d4 100644 --- a/src/test/java/com/firebolt/jdbc/util/PropertyUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/util/PropertyUtilTest.java @@ -1,5 +1,6 @@ package com.firebolt.jdbc.util; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mockStatic; @@ -23,14 +24,14 @@ void shouldGetPropertyInfo() { FireboltSessionProperty.BUFFER_SIZE); mocked.when(FireboltSessionProperty::getNonDeprecatedProperties).thenReturn(existingProperties); DriverPropertyInfo accountDriverInfo = createExpectedDriverInfo(FireboltSessionProperty.ACCOUNT.getKey(), - FireboltSessionProperty.ACCOUNT.getDescription(), null); + FireboltSessionProperty.ACCOUNT.getDescription(), "myAccount"); DriverPropertyInfo bufferDriverInfo = createExpectedDriverInfo(FireboltSessionProperty.BUFFER_SIZE.getKey(), FireboltSessionProperty.BUFFER_SIZE.getDescription(), "1"); DriverPropertyInfo[] expected = new DriverPropertyInfo[] { accountDriverInfo, bufferDriverInfo }; for (int i = 0; i < expected.length; i++) { assertTrue(new ReflectionEquals( - PropertyUtil.getPropertyInfo("jdbc:firebolt://api.dev.firebolt.io/Tutorial_11_04?buffer_size=1", + PropertyUtil.getPropertyInfo("jdbc:firebolt:Tutorial_11_04/?buffer_size=1&account=myAccount", new Properties())[i]).matches(expected[i])); } } diff --git a/src/test/java/com/firebolt/jdbc/util/StatementUtilTest.java b/src/test/java/com/firebolt/jdbc/util/StatementUtilTest.java index f051ea3e..c688a676 100644 --- a/src/test/java/com/firebolt/jdbc/util/StatementUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/util/StatementUtilTest.java @@ -1,12 +1,15 @@ package com.firebolt.jdbc.util; +import com.firebolt.jdbc.connection.FireboltConnection; import com.firebolt.jdbc.statement.ParamMarker; import com.firebolt.jdbc.statement.StatementUtil; import com.firebolt.jdbc.statement.rawstatement.QueryRawStatement; +import com.firebolt.jdbc.statement.rawstatement.RawStatement; import com.firebolt.jdbc.statement.rawstatement.RawStatementWrapper; import com.firebolt.jdbc.statement.rawstatement.SetParamRawStatement; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import java.io.BufferedReader; import java.io.InputStream; @@ -22,11 +25,15 @@ import static com.firebolt.jdbc.statement.StatementType.PARAM_SETTING; import static com.firebolt.jdbc.statement.StatementUtil.isQuery; +import static com.firebolt.jdbc.statement.StatementUtil.parseToRawStatementWrapper; import static com.firebolt.jdbc.statement.StatementUtil.replaceParameterMarksWithValues; +import static com.firebolt.jdbc.statement.rawstatement.StatementValidatorFactory.createValidator; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; class StatementUtilTest { @@ -39,7 +46,7 @@ private static String getSqlFromFile(String path) { @Test void shouldExtractAdditionalProperties() { String query = "set my_custom_query=1"; - assertEquals(Optional.of(new ImmutablePair<>("my_custom_query", "1")), + assertEquals(Optional.of(Map.entry("my_custom_query", "1")), StatementUtil.extractParamFromSetStatement(query, null)); } @@ -47,30 +54,61 @@ void shouldExtractAdditionalProperties() { void shouldExtractAdditionalPropertiesWithComments() { String query = "/* */" + " SeT my_custom_query=1"; String cleanQuery = "SeT my_custom_query=1"; - assertEquals(Optional.of(new ImmutablePair<>("my_custom_query", "1")), + assertEquals(Optional.of(Map.entry("my_custom_query", "1")), StatementUtil.extractParamFromSetStatement(cleanQuery, query)); } @Test void shouldExtractAdditionalWithEmptyProperties() { String query = "set my_custom_char=' '"; - assertEquals(Optional.of(new ImmutablePair<>("my_custom_char", " ")), + assertEquals(Optional.of(Map.entry("my_custom_char", " ")), StatementUtil.extractParamFromSetStatement(query, null)); } @Test void shouldExtractTimezone() { String query = "set time_zone='Europe/Berlin';"; - assertEquals(Optional.of(new ImmutablePair<>("time_zone", "Europe/Berlin")), + assertEquals(Optional.of(Map.entry("time_zone", "Europe/Berlin")), StatementUtil.extractParamFromSetStatement(query, null)); } + @Test + void shouldReturnEmptyListForEmptySql() { + assertTrue(parseToRawStatementWrapper("").getSubStatements().isEmpty()); + } + + @ParameterizedTest + @CsvSource(value = { + "1,set database='my_db',Could not set parameter. Set parameter 'DATABASE' is not allowed. Try again with 'USE DATABASE' instead of SET.", + "1,set engine='my_engine',Could not set parameter. Set parameter 'ENGINE' is not allowed. Try again with 'USE ENGINE' instead of SET.", + "1,set account_id='123',Could not set parameter. Set parameter 'ACCOUNT_ID' is not allowed. Try again with a different parameter name.", + "1,set output_format='XML',Could not set parameter. Set parameter 'OUTPUT_FORMAT' is not allowed. Try again with a different parameter name.", + "2,set database='my_db',Could not set parameter. Set parameter 'DATABASE' is not allowed. Try again with 'USE DATABASE' instead of SET.", + "2,set engine='my_engine',Could not set parameter. Set parameter 'ENGINE' is not allowed. Try again with 'USE ENGINE' instead of SET.", + "2,set output_format='XML',Could not set parameter. Set parameter 'OUTPUT_FORMAT' is not allowed. Try again with a different parameter name." + }) + void shouldThrowWhenSettingPropertyThatShouldBeSetByUse(int infraVersion, String statement, String error) { + FireboltConnection connection = mock(FireboltConnection.class); + when(connection.getInfraVersion()).thenReturn(infraVersion); + List rawStatements = parseToRawStatementWrapper(statement).getSubStatements(); + IllegalArgumentException e = assertThrows( + IllegalArgumentException.class, + () -> rawStatements.forEach(s -> createValidator(s, connection).validate(s))); + assertEquals(error, e.getMessage()); + } + + @Test + void shouldSuccessfullySetAccountIdForInfraVersion2() { + FireboltConnection connection = mock(FireboltConnection.class); + when(connection.getInfraVersion()).thenReturn(2); + parseToRawStatementWrapper("set account_id='123'").getSubStatements().forEach(s -> createValidator(s, connection).validate(s)); + } + @Test void shouldFindThatStatementsWithQueryKeywordsAreQueries() { List keywords = Arrays.asList("shOW", "seleCt", "DESCRIBE", "exists", "explain", "with", "call"); String query = "/* Some random command*/ -- oneLineOfComment INSERT \n %s anything"; - keywords.forEach(keyword -> assertTrue(isQuery(StatementUtil - .parseToRawStatementWrapper(String.format(query, keyword)).getSubStatements().get(0).getCleanSql()))); + keywords.forEach(keyword -> assertTrue(isQuery(parseToRawStatementWrapper(String.format(query, keyword)).getSubStatements().get(0).getCleanSql()))); } @Test @@ -85,7 +123,7 @@ void shouldExtractTableNameFromQuery() { String query = "/* Some random comment*/ SELECT /* Second comment */ * FROM -- third comment \n EMPLOYEES WHERE id = 5"; assertEquals("EMPLOYEES", - ((QueryRawStatement) StatementUtil.parseToRawStatementWrapper(query).getSubStatements().get(0)) + ((QueryRawStatement) parseToRawStatementWrapper(query).getSubStatements().get(0)) .getTable()); } @@ -93,7 +131,7 @@ void shouldExtractTableNameFromQuery() { void shouldExtractDbNameFromQuery() { String query = "-- Some random command \n SELECT * FROM db.schema.EMPLOYEES WHERE id = 5"; assertEquals("db", - ((QueryRawStatement) StatementUtil.parseToRawStatementWrapper(query).getSubStatements().get(0)) + ((QueryRawStatement) parseToRawStatementWrapper(query).getSubStatements().get(0)) .getDatabase()); } @@ -101,7 +139,7 @@ void shouldExtractDbNameFromQuery() { void shouldBeEmptyWhenGettingDbNameAndThereIsNoDbName() { String query = "/* Some random command*/ SELECT * FROM EMPLOYEES WHERE id = 5"; assertEquals(null, - ((QueryRawStatement) StatementUtil.parseToRawStatementWrapper(query).getSubStatements().get(0)) + ((QueryRawStatement) parseToRawStatementWrapper(query).getSubStatements().get(0)) .getDatabase()); } @@ -109,29 +147,29 @@ void shouldBeEmptyWhenGettingDbNameAndThereIsNoDbName() { void shouldBeEmptyWhenGettingDbNameFromAQueryWithoutFrom() { String query = "SELECT *"; assertEquals(null, - ((QueryRawStatement) StatementUtil.parseToRawStatementWrapper(query).getSubStatements().get(0)) + ((QueryRawStatement) parseToRawStatementWrapper(query).getSubStatements().get(0)) .getDatabase()); } @Test void shouldGetEmptyDbNameAndTablesTableNameWhenUsingDescribe() { String query = "DESCRIBE EMPLOYEES"; - assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getLeft()); + assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getKey()); assertEquals(Optional.of("tables"), - StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getRight()); + StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getValue()); } @Test void shouldGetEmptyTableNameAndEmptyDbNameWhenUsingShow() { String query = "SHOW databases"; - assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getLeft()); - assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getRight()); + assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getKey()); + assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getValue()); } @Test void shouldBeEmptyWhenGettingTableNameWhenTheQueryIsNotASelect() { String query = "/* Some random command*/ UPDATE * FROM EMPLOYEES WHERE id = 5"; - assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getRight()); + assertEquals(Optional.empty(), StatementUtil.extractDbNameAndTableNamePairFromCleanQuery(query).getValue()); } @Test @@ -144,10 +182,10 @@ void shouldThrowAnExceptionWhenTheSetCannotBeParsed() { void shouldCleanQueryWithComments() { String sql = getSqlFromFile("/queries/query-with-comment.sql"); String expectedCleanQuery = getSqlFromFile("/queries/query-with-comment-cleaned.sql"); - String cleanStatement = StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql(); + String cleanStatement = parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql(); assertEquals(expectedCleanQuery, cleanStatement); assertEquals(expectedCleanQuery, - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); } @Test @@ -157,7 +195,7 @@ void shouldCleanQueryWithQuotesInTheVarchar() { String expectedCleanQuery = "INSERT INTO regex_test (name)\n" + "\n" + "VALUES (\n" + "'Taylor''s Prime Steak House 3' )"; assertEquals(expectedCleanQuery, - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); } @@ -165,76 +203,76 @@ void shouldCleanQueryWithQuotesInTheVarchar() { void shouldCleanQueryWithSingleLineComment() { String sql = getSqlFromFile("/queries/query-with-comment.sql"); String expectedCleanQuery = getSqlFromFile("/queries/query-with-comment-cleaned.sql"); - String cleanStatement = StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql(); + String cleanStatement = parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql(); assertEquals(expectedCleanQuery, cleanStatement); assertEquals(expectedCleanQuery, - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); } @Test void shouldCountParametersFromLongQueryWithComments() { String sql = getSqlFromFile("/queries/query-with-comment.sql"); - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql(); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql(); assertEquals(Arrays.asList(new ParamMarker(1, 200)), - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getParamMarkers()); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getParamMarkers()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParams() { String sql = "SElECT * FROM EMPLOYEES WHERE id = ?"; assertEquals(Map.of(1, 35), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsWithoutTrimmingRequest() { String sql = " SElECT * FROM EMPLOYEES WHERE id = ?"; assertEquals(Map.of(1, 40), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsFromIn() { String sql = "SElECT * FROM EMPLOYEES WHERE id IN (?,?)"; assertEquals(Map.of(1, 37, 2, 39), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsThatAreNotInComments() { String sql = "SElECT * FROM EMPLOYEES WHERE /* ?*/id IN (?,?)"; assertEquals(Map.of(1, 43, 2, 45), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsThatAreNotInComments2() { String sql = "SElECT * FROM EMPLOYEES WHERE /* ?id IN (?,?)*/"; assertEquals(Map.of(), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsThatAreNotInSingleLineComment() { String sql = "SElECT * FROM EMPLOYEES WHERE id IN --(?,?)\n"; assertEquals(Map.of(), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsThatAreNotInSingleLineComment2() { String sql = "SElECT * FROM EMPLOYEES WHERE id IN --\n(?,?)"; assertEquals(Map.of(1, 40, 2, 42), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldGetAllQueryParamsThatAreNotInBetweenQuotesOrComments() { String sql = "SElECT * FROM EMPLOYEES WHERE id IN --(?,?)\n AND name NOT LIKE '? Hello ? ' AND address LIKE ? AND my_date = ?"; assertEquals(Map.of(1, 93, 2, 109), StatementUtil.getParamMarketsPositions(sql)); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test @@ -304,7 +342,7 @@ void shouldThrowExceptionWhenTheParameterProvidedIsNull() { @Test void shouldCountOnlyTwoQueriesWhenTheLastPartOfTheLastQueryIsAComment() { String sql = "SElECT * FROM EMPLOYEES WHERE id = ? AND name LIKE ? AND dob = ? ; SELECT 1; --Some comment"; - assertEquals(2, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(2, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test @@ -312,10 +350,10 @@ void shouldIncludeCommentsAndSemicolonsInSubQueries() { String sql = "SElECT * FROM EMPLOYEES WHERE id = ? AND name LIKE ? AND dob = ? --Fetch employee with provided id \n ;\n\n\n\n\n SELECT 1;;;;;;;;;;;;; --Some comment"; assertEquals( "SElECT * FROM EMPLOYEES WHERE id = ? AND name LIKE ? AND dob = ? --Fetch employee with provided id \n ;\n\n\n\n\n ", - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql()); assertEquals("SELECT 1;;;;;;;;;;;;; --Some comment", - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(1).getSql()); - assertEquals(2, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + parseToRawStatementWrapper(sql).getSubStatements().get(1).getSql()); + assertEquals(2, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test @@ -323,8 +361,8 @@ void shouldNotConsiderCommentAsAStatementEvenWhenItEndsWithSemicolon() { String sql = "\n\n\n -- PARTITION BY \"name\";\n" + "\n" + "INSERT INTO employees VALUES(\n" + " 1,\n" + " 'hello',\n" + " 'world',\n" + " 'site',\n" + ");"; - assertEquals(sql, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql()); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(sql, parseToRawStatementWrapper(sql).getSubStatements().get(0).getSql()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @@ -332,18 +370,18 @@ void shouldNotConsiderCommentAsAStatementEvenWhenItEndsWithSemicolon() { void shouldFindOnlyOneQueryAndNoParamInHugeStatement() { String sql = getSqlFromFile("/queries/query-with-huge-select.sql"); assertEquals(Collections.emptyList(), - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getParamMarkers()); - assertEquals(1, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getParamMarkers()); + assertEquals(1, parseToRawStatementWrapper(sql).getSubStatements().size()); } @Test void shouldCountOnlyTwoSubStatementsInMultiStatementWithALotOfComments() { String sql = " --Getting Multiple RS;\nSELECT 1; /* comment 1 ; ; ; */\n\n --Another comment ; \n -- ; \n SELECT 2; /* comment 2 */"; - assertEquals(2, StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().size()); + assertEquals(2, parseToRawStatementWrapper(sql).getSubStatements().size()); assertEquals("SELECT 1;", - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(0).getCleanSql()); assertEquals("SELECT 2;", - StatementUtil.parseToRawStatementWrapper(sql).getSubStatements().get(1).getCleanSql()); + parseToRawStatementWrapper(sql).getSubStatements().get(1).getCleanSql()); } @Test @@ -367,9 +405,9 @@ void shouldThrowExceptionWhenTryingToSetAParamWithAnInvalidPosition() { @Test void shouldParseStatementWithTypeParamSettingForStatementThatSetAdditionalParam() { String sql = "-- comment \n \n SET x = 1 ;"; - RawStatementWrapper rawStatementWrapper = StatementUtil.parseToRawStatementWrapper(sql); + RawStatementWrapper rawStatementWrapper = parseToRawStatementWrapper(sql); assertEquals(PARAM_SETTING, rawStatementWrapper.getSubStatements().get(0).getStatementType()); - assertEquals(ImmutablePair.of("x", "1"), + assertEquals(Map.entry("x", "1"), ((SetParamRawStatement) rawStatementWrapper.getSubStatements().get(0)).getAdditionalProperty()); } @@ -390,7 +428,7 @@ void shouldParseStatementWithQuestionMarksBetweenDoubleQuotes() { + " \"test_data_venues\".\"price\" AS \"price\",\n" + " (\"test_data_venues\".\"price\" * -1) AS \"Refund Amount (?)\"\n" + " FROM \"test_data_venues\") \"source\"\n" + "ORDER BY \"source\".\"id\" ASC LIMIT 1"; - RawStatementWrapper rawStatementWrapper = StatementUtil.parseToRawStatementWrapper(sql); + RawStatementWrapper rawStatementWrapper = parseToRawStatementWrapper(sql); assertEquals(0, rawStatementWrapper.getTotalParams()); } diff --git a/src/test/java/com/firebolt/jdbc/util/StringUtilTest.java b/src/test/java/com/firebolt/jdbc/util/StringUtilTest.java new file mode 100644 index 00000000..4e07d47b --- /dev/null +++ b/src/test/java/com/firebolt/jdbc/util/StringUtilTest.java @@ -0,0 +1,65 @@ +package com.firebolt.jdbc.util; + + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class StringUtilTest { + @ParameterizedTest + @CsvSource(value = { + ",", + "``,``", + "'',``", + "1,1", + "true,true", + "'hello',hello" + }, + delimiter = ',', + quoteCharacter = '`') + void stripSingleQuotes(String value, String expected) { + assertEquals(expected, StringUtil.strip(value, '\'')); + } + + @Test + void splitNull() { + splitAllByTab(null, new String[0]); + } + + @Test + void splitEmpty() { + splitAllByTab("", new String[] { "" }); + } + + @Test + void splitSingle() { + splitAllByTab("hello", new String[] {"hello"}); + } + + @Test + void splitTwo() { + splitAllByTab("hello\tbye", new String[] {"hello", "bye"}); + } + + @Test + void splitTwoNullTerminated() { + splitAllByTab("hello\t", new String[] {"hello", ""}); + } + + @Test + void splitTwoNullStarted() { + splitAllByTab("\thello", new String[] {"", "hello"}); + } + + @Test + void splitConsequentTabs() { + splitAllByTab("prefix\t\tsuffix", new String[] {"prefix", "", "suffix"}); + } + + private void splitAllByTab(String str, String[] expected) { + assertArrayEquals(expected, StringUtil.splitAll(str, '\t')); + } +} \ No newline at end of file diff --git a/src/test/java/com/firebolt/jdbc/util/VersionUtilTest.java b/src/test/java/com/firebolt/jdbc/util/VersionUtilTest.java index a6f435a6..dbb4257e 100644 --- a/src/test/java/com/firebolt/jdbc/util/VersionUtilTest.java +++ b/src/test/java/com/firebolt/jdbc/util/VersionUtilTest.java @@ -8,17 +8,17 @@ class VersionUtilTest { @Test void shouldGetDriverMajorVersion() { - assertEquals(2, VersionUtil.getMajorDriverVersion()); + assertEquals(3, VersionUtil.getMajorDriverVersion()); } @Test void shouldGetDriverMinorVersion() { - assertEquals(4, VersionUtil.getDriverMinorVersion()); + assertEquals(0, VersionUtil.getDriverMinorVersion()); } @Test void shouldGetProjectVersion() { - assertEquals("2.4.6", VersionUtil.getDriverVersion()); + assertEquals("3.0.4-SNAPSHOT", VersionUtil.getDriverVersion()); } @Test diff --git a/src/test/resources/responses/firebolt-response-example b/src/test/resources/responses/firebolt-response-example index a033f0a4..5ae23519 100644 --- a/src/test/resources/responses/firebolt-response-example +++ b/src/test/resources/responses/firebolt-response-example @@ -1,4 +1,4 @@ -id arr name a_date is_online a_double an_integer -int32 Array(Array(Array(String))) Nullable(String) Date Boolean Float32 Int64 -1 [[['1','2'],['3','4']]] Taylor\'s Prime Steak House 2022-05-10 1 14.6 5 -2 [[['1','2'],['3','4']],[['5','6'],['7','8',NULL]]] \N 2022-05-10 0 \N \N \ No newline at end of file +id arr name a_date is_online a_double an_integer url +int32 Array(Array(Array(String))) Nullable(String) Date Boolean Float32 Int64 Nullable(String) +1 [[['1','2'],['3','4']]] Taylor\'s Prime Steak House 2022-05-10 1 14.6 5 http://firebolt.io +2 [[['1','2'],['3','4']],[['5','6'],['7','8',NULL]]] \N 2022-05-10 0 \N \N \N \ No newline at end of file diff --git a/src/test/resources/responses/firebolt-response-with-bytea b/src/test/resources/responses/firebolt-response-with-bytea index 85c1c0d1..1d231002 100644 --- a/src/test/resources/responses/firebolt-response-with-bytea +++ b/src/test/resources/responses/firebolt-response-with-bytea @@ -1,4 +1,4 @@ -id null_bytea an_empty_bytea a_bytea -Int64 bytea bytea bytea -1 \N \xdeadbeef -2 \N \x00ab \ No newline at end of file +id null_bytea an_empty_bytea a_bytea false_bytea +Int64 bytea bytea bytea bytea +1 \N \xdeadbeef 1 +2 \N \x00ab hello \ No newline at end of file diff --git a/src/test/resources/responses/firebolt-response-with-infinity b/src/test/resources/responses/firebolt-response-with-infinity index e8d82cc8..f54ff4e0 100644 --- a/src/test/resources/responses/firebolt-response-with-infinity +++ b/src/test/resources/responses/firebolt-response-with-infinity @@ -1,3 +1,3 @@ -pos neg -int64 int64 -inf -inf +pos neg nan neg_nan +int64 int64 int int +inf -inf nan -nan diff --git a/src/test/resources/responses/firebolt-response-with-numeric-types.csv b/src/test/resources/responses/firebolt-response-with-numeric-types.csv index 06d562b7..206d1684 100644 --- a/src/test/resources/responses/firebolt-response-with-numeric-types.csv +++ b/src/test/resources/responses/firebolt-response-with-numeric-types.csv @@ -1,3 +1,3 @@ uint8 uint64 float32 float64 decimal an_int a_long a_float a_double a_decimal integer bigint real double precision numeric(38, 30) int long float double Decimal(38, 30) -1 30000000000 1.23 1.23456789012 1231232.123459999990457054844258706536 80000 30000000000 1.23 1.23456789012 1231232.123459999990457054844258706536 \ No newline at end of file +1 30000000000 1.23 1.23456789012 1231232.123459999990457054844258706536 30000 30000000000 1.23 1.23456789012 1231232.123459999990457054844258706536 \ No newline at end of file diff --git a/src/test/resources/responses/metadata/firebolt-response-get-schemas-example b/src/test/resources/responses/metadata/firebolt-response-get-schemas-example new file mode 100644 index 00000000..a317c194 --- /dev/null +++ b/src/test/resources/responses/metadata/firebolt-response-get-schemas-example @@ -0,0 +1,4 @@ +TABLE_SCHEM TABLE_CATALOG +String String +public my-db +information_schema my-db