From e42d0560504f99351c2d0a811b56c7a0ffcf271b Mon Sep 17 00:00:00 2001 From: Rodi Reich Zilberman <867491+rodireich@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:39:55 -0800 Subject: [PATCH] merge mysql v2 into v1 (#48369) --- .../connectors_version_increment_check.yml | 2 +- .../connectors/source-mysql-v2/build.gradle | 23 - .../source-mysql-v2/gradle.properties | 2 - .../connectors/source-mysql-v2/icon.svg | 1 - .../connectors/source-mysql-v2/metadata.yaml | 28 - .../source-mysql/acceptance-test-config.yml | 98 -- .../connectors/source-mysql/build.gradle | 55 +- .../connectors/source-mysql/gradle.properties | 3 +- .../source-mysql/integration_tests/Dockerfile | 13 - .../abnormal_state_template.json | 20 - .../integration_tests/acceptance.py | 16 - .../configured_catalog_template.json | 28 - .../integration_tests/expected_records.txt | 3 - ...cremental_configured_catalog_template.json | 28 - .../integration_tests/seed/basic.sql | 153 --- .../integration_tests/seed/full.sql | 528 ---------- .../seed/full_without_nulls.sql | 306 ------ .../integration_tests/seed/hook.py | 209 ---- .../integration_tests/seed/requirements.txt | 2 - .../connectors/source-mysql/metadata.yaml | 60 +- .../source/mysql/MySqlQueryUtils.java | 237 ----- .../source/mysql/MySqlSource.java | 669 ------------- .../source/mysql/MySqlSourceOperations.java | 302 ------ .../source/mysql/MySqlSpecConstants.java | 15 - .../mysql/MySqlStreamingQueryConfig.java | 28 - .../mysql/cdc/CdcConfigurationHelper.java | 102 -- ...stomMySQLTinyIntOneToBooleanConverter.java | 21 - .../mysql/cdc/MySQLDateTimeConverter.java | 93 -- .../MySqlCdcConnectorMetadataInjector.java | 73 -- .../source/mysql/cdc/MySqlCdcPosition.java | 37 - .../source/mysql/cdc/MySqlCdcProperties.java | 139 --- .../mysql/cdc/MySqlCdcSavedInfoFetcher.java | 41 - .../mysql/cdc/MySqlCdcStateHandler.java | 70 -- .../mysql/cdc/MySqlCdcTargetPosition.java | 149 --- .../mysql/cdc/MySqlDebeziumStateUtil.java | 371 ------- .../mysql/cdc/MysqlCdcStateConstants.java | 14 - .../MySqlCursorBasedStateManager.java | 86 -- .../initialsync/CdcMetadataInjector.java | 29 - .../MySqlInitialLoadGlobalStateManager.java | 203 ---- .../initialsync/MySqlInitialLoadHandler.java | 223 ----- .../MySqlInitialLoadRecordIterator.java | 231 ----- .../MySqlInitialLoadStateManager.java | 90 -- .../MySqlInitialLoadStreamStateManager.java | 79 -- .../initialsync/MySqlInitialReadUtil.java | 582 ----------- .../kotlin/MySqlSourceExceptionHandler.kt | 103 -- .../MysqlCdcInitialSnapshotStateValue.kt | 0 .../source/mysql/MysqlCdcMetaFields.kt | 0 .../source/mysql/MysqlJdbcEncryption.kt | 0 .../source/mysql/MysqlJdbcPartition.kt | 0 .../source/mysql/MysqlJdbcPartitionFactory.kt | 0 .../source/mysql/MysqlJdbcStreamStateValue.kt | 0 .../integrations/source/mysql/MysqlSource.kt | 0 .../source/mysql/MysqlSourceConfiguration.kt | 0 .../MysqlSourceConfigurationSpecification.kt | 0 .../mysql/MysqlSourceMetadataQuerier.kt | 0 .../source/mysql/MysqlSourceOperations.kt | 0 .../mysql/cdc/MySqlDebeziumOperations.kt | 0 .../source/mysql/cdc/MySqlPosition.kt | 0 .../cdc/converters/MySQLBooleanConverter.kt | 0 .../cdc/converters/MySQLDateTimeConverter.kt | 0 .../cdc/converters/MySQLNumbericConverter.kt | 0 .../src/main/resources/application.yml | 0 .../internal_models/internal_models.yaml | 48 - .../source-mysql/src/main/resources/spec.json | 252 ----- .../AbstractMySqlSourceDatatypeTest.java | 501 ---------- .../AbstractSshMySqlSourceAcceptanceTest.java | 87 -- .../sources/CDCMySqlDatatypeAccuracyTest.java | 37 - .../CdcBinlogsMySqlSourceDatatypeTest.java | 115 --- ...nitialSnapshotMySqlSourceDatatypeTest.java | 34 - .../sources/CdcMySqlSourceAcceptanceTest.java | 254 ----- ...lSslCaCertificateSourceAcceptanceTest.java | 31 - ...cMySqlSslRequiredSourceAcceptanceTest.java | 35 - ...udDeploymentMySqlSourceAcceptanceTest.java | 26 - ...lSslCaCertificateSourceAcceptanceTest.java | 50 - ...slFullCertificateSourceAcceptanceTest.java | 52 - .../sources/MySqlDatatypeAccuracyTest.java | 475 --------- .../sources/MySqlSourceAcceptanceTest.java | 161 --- .../sources/MySqlSourceDatatypeTest.java | 37 - ...lSslCaCertificateSourceAcceptanceTest.java | 34 - ...slFullCertificateSourceAcceptanceTest.java | 36 - .../sources/MySqlSslSourceAcceptanceTest.java | 21 - .../SshKeyMySqlSourceAcceptanceTest.java | 16 - .../SshPasswordMySqlSourceAcceptanceTest.java | 49 - .../resources/dummy_config.json | 7 - .../resources/expected_cloud_spec.json | 343 ------- .../resources/expected_oss_spec.json | 367 ------- .../src/test-integration/resources/test.png | Bin 17018 -> 0 bytes .../mysql/CdcConfigurationHelperTest.java | 33 - .../source/mysql/CdcMysqlSourceTest.java | 930 ------------------ .../CdcMysqlSourceWithSpecialDbNameTest.java | 39 - .../mysql/CloudDeploymentMySqlSslTest.java | 149 --- .../source/mysql/MySqlDebugger.java | 16 - .../mysql/MySqlInitialLoadHandlerTest.java | 36 - .../mysql/MySqlJdbcSourceAcceptanceTest.java | 535 ---------- .../mysql/MySqlSourceOperationsTest.java | 131 --- .../source/mysql/MySqlSourceTests.java | 158 --- .../MySqlSslJdbcSourceAcceptanceTest.java | 31 - .../source/mysql/MySqlStressTest.java | 107 -- .../mysql/MysqlDebeziumStateUtilTest.java | 185 ---- .../kotlin/MySqlSourceExceptionHandlerTest.kt | 42 - .../mysql/MysqlCdcDatatypeIntegrationTest.kt | 0 .../source/mysql/MysqlCdcIntegrationTest.kt | 0 .../source/mysql/MysqlContainerFactory.kt | 0 .../mysql/MysqlCursorBasedIntegrationTest.kt | 0 .../mysql/MysqlJdbcPartitionFactoryTest.kt | 0 ...sqlSourceConfigurationSpecificationTest.kt | 0 .../mysql/MysqlSourceConfigurationTest.kt | 0 .../MysqlSourceDatatypeIntegrationTest.kt | 0 .../MysqlSourceSelectQueryGeneratorTest.kt | 0 .../MysqlSourceTestConfigurationFactory.kt | 0 .../source/mysql/MysqlSpecIntegrationTest.kt | 0 .../src/test/resources/expected-spec.json | 0 .../test/resources/expected_cloud_spec.json | 246 ----- .../src/test/resources/expected_oss_spec.json | 252 ----- .../source/mysql/MySQLContainerFactory.java | 70 -- .../source/mysql/MySQLTestDatabase.java | 182 ---- docs/integrations/sources/mysql.md | 7 +- 117 files changed, 31 insertions(+), 11751 deletions(-) delete mode 100644 airbyte-integrations/connectors/source-mysql-v2/build.gradle delete mode 100644 airbyte-integrations/connectors/source-mysql-v2/gradle.properties delete mode 100644 airbyte-integrations/connectors/source-mysql-v2/icon.svg delete mode 100644 airbyte-integrations/connectors/source-mysql-v2/metadata.yaml delete mode 100644 airbyte-integrations/connectors/source-mysql/acceptance-test-config.yml delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/Dockerfile delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/abnormal_state_template.json delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/acceptance.py delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/configured_catalog_template.json delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/expected_records.txt delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/incremental_configured_catalog_template.json delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/seed/basic.sql delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/seed/full.sql delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/seed/full_without_nulls.sql delete mode 100755 airbyte-integrations/connectors/source-mysql/integration_tests/seed/hook.py delete mode 100644 airbyte-integrations/connectors/source-mysql/integration_tests/seed/requirements.txt delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlQueryUtils.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSourceOperations.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSpecConstants.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlStreamingQueryConfig.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CdcConfigurationHelper.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CustomMySQLTinyIntOneToBooleanConverter.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySQLDateTimeConverter.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcConnectorMetadataInjector.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcPosition.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcProperties.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcSavedInfoFetcher.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcStateHandler.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcTargetPosition.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumStateUtil.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MysqlCdcStateConstants.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cursor_based/MySqlCursorBasedStateManager.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/CdcMetadataInjector.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadHandler.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadRecordIterator.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStreamStateManager.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialReadUtil.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/kotlin/MySqlSourceExceptionHandler.kt rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcInitialSnapshotStateValue.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcMetaFields.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartition.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactory.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcStreamStateValue.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSource.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecification.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceMetadataQuerier.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceOperations.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumOperations.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlPosition.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLBooleanConverter.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLDateTimeConverter.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLNumbericConverter.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/main/resources/application.yml (100%) delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/resources/internal_models/internal_models.yaml delete mode 100644 airbyte-integrations/connectors/source-mysql/src/main/resources/spec.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractMySqlSourceDatatypeTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractSshMySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CDCMySqlDatatypeAccuracyTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcBinlogsMySqlSourceDatatypeTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcInitialSnapshotMySqlSourceDatatypeTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslCaCertificateSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslRequiredSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslCaCertificateSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslFullCertificateSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlDatatypeAccuracyTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceDatatypeTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslCaCertificateSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslFullCertificateSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshKeyMySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshPasswordMySqlSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/resources/dummy_config.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_cloud_spec.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_oss_spec.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test-integration/resources/test.png delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcConfigurationHelperTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceWithSpecialDbNameTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CloudDeploymentMySqlSslTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlDebugger.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlInitialLoadHandlerTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceOperationsTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceTests.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSslJdbcSourceAcceptanceTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlStressTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MysqlDebeziumStateUtilTest.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/kotlin/MySqlSourceExceptionHandlerTest.kt rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcDatatypeIntegrationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcIntegrationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlContainerFactory.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCursorBasedIntegrationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactoryTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecificationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceSelectQueryGeneratorTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceTestConfigurationFactory.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSpecIntegrationTest.kt (100%) rename airbyte-integrations/connectors/{source-mysql-v2 => source-mysql}/src/test/resources/expected-spec.json (100%) delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/resources/expected_cloud_spec.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/test/resources/expected_oss_spec.json delete mode 100644 airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLContainerFactory.java delete mode 100644 airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLTestDatabase.java diff --git a/.github/workflows/connectors_version_increment_check.yml b/.github/workflows/connectors_version_increment_check.yml index bde0e7967b1c..cd35b22de657 100644 --- a/.github/workflows/connectors_version_increment_check.yml +++ b/.github/workflows/connectors_version_increment_check.yml @@ -23,7 +23,7 @@ jobs: name: Connectors Version Increment Check runs-on: connector-test-large if: github.event.pull_request.head.repo.fork != true - timeout-minutes: 12 + timeout-minutes: 22 steps: - name: Checkout Airbyte uses: actions/checkout@v4 diff --git a/airbyte-integrations/connectors/source-mysql-v2/build.gradle b/airbyte-integrations/connectors/source-mysql-v2/build.gradle deleted file mode 100644 index a11a6e59f352..000000000000 --- a/airbyte-integrations/connectors/source-mysql-v2/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id 'airbyte-bulk-connector' -} - -application { - mainClass = 'io.airbyte.integrations.source.mysql.MysqlSource' -} - -airbyteBulkConnector { - core = 'extract' - toolkits = ['extract-jdbc', 'extract-cdc'] - cdk = 'local' -} - -dependencies { - implementation 'mysql:mysql-connector-java:8.0.30' - implementation 'org.codehaus.plexus:plexus-utils:4.0.0' - implementation 'io.debezium:debezium-connector-mysql' - - testImplementation platform('org.testcontainers:testcontainers-bom:1.20.2') - testImplementation 'org.testcontainers:mysql' - testImplementation("io.mockk:mockk:1.12.0") -} diff --git a/airbyte-integrations/connectors/source-mysql-v2/gradle.properties b/airbyte-integrations/connectors/source-mysql-v2/gradle.properties deleted file mode 100644 index 04dcba8cefd7..000000000000 --- a/airbyte-integrations/connectors/source-mysql-v2/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -testExecutionConcurrency=1 -JunitMethodExecutionTimeout=5m \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql-v2/icon.svg b/airbyte-integrations/connectors/source-mysql-v2/icon.svg deleted file mode 100644 index 607d361ed765..000000000000 --- a/airbyte-integrations/connectors/source-mysql-v2/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml b/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml deleted file mode 100644 index 0e554de99713..000000000000 --- a/airbyte-integrations/connectors/source-mysql-v2/metadata.yaml +++ /dev/null @@ -1,28 +0,0 @@ -data: - ab_internal: - ql: 200 - sl: 0 - allowedHosts: - hosts: - - ${host} - - ${tunnel_method.tunnel_host} - connectorSubtype: database - connectorType: source - definitionId: 561393ed-7e3a-4d0d-8b8b-90ded371754c - dockerImageTag: 0.0.32 - dockerRepository: airbyte/source-mysql-v2 - documentationUrl: https://docs.airbyte.com/integrations/sources/mysql - githubIssueLabel: source-mysql-v2 - icon: mysql.svg - license: ELv2 - name: Mysqlv2 Source - registryOverrides: - cloud: - enabled: false - oss: - enabled: false - releaseStage: alpha - supportLevel: community - tags: - - language:java -metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-mysql/acceptance-test-config.yml b/airbyte-integrations/connectors/source-mysql/acceptance-test-config.yml deleted file mode 100644 index aa83992499d9..000000000000 --- a/airbyte-integrations/connectors/source-mysql/acceptance-test-config.yml +++ /dev/null @@ -1,98 +0,0 @@ -# See [Connector Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/connector-acceptance-tests-reference) -# for more information about how to configure these tests -connector_image: airbyte/source-mysql:dev -custom_environment_variables: - USE_STREAM_CAPABLE_STATE: true -acceptance_tests: - client_container_config: - client_container_dockerfile_path: "integration_tests/Dockerfile" - final_teardown_command: - - "python" - - "./hook.py" - - "final_teardown" - spec: - tests: - - spec_path: "src/test-integration/resources/expected_oss_spec.json" - config_path: "secrets/cat-config.json" - - spec_path: "src/test-integration/resources/expected_cloud_spec.json" - config_path: "secrets/cat-config.json" - deployment_mode: cloud - connection: - tests: - - config_path: "secrets/cat-config.json" - status: "succeed" - discovery: - tests: - - config_path: "secrets/cat-config.json" - basic_read: - tests: - - config_path: "integration_tests/temp/config_active.json" - configured_catalog_path: "integration_tests/temp/configured_catalog_copy.json" - expect_records: - path: "integration_tests/expected_records.txt" - client_container_config: - client_container_dockerfile_path: "integration_tests/Dockerfile" - secrets_path: "secrets/cat-config.json" - setup_command: - - "python" - - "./hook.py" - - "setup" - teardown_command: - - "python" - - "./hook.py" - - "teardown" - full_refresh: - tests: - - config_path: "integration_tests/temp/config_active.json" - configured_catalog_path: "integration_tests/temp/configured_catalog_copy.json" - client_container_config: - client_container_dockerfile_path: "integration_tests/Dockerfile" - secrets_path: "secrets/cat-config.json" - setup_command: - - "python" - - "./hook.py" - - "setup" - teardown_command: - - "python" - - "./hook.py" - - "teardown" - incremental: - tests: - - config_path: "integration_tests/temp/config_active.json" - configured_catalog_path: "integration_tests/temp/incremental_configured_catalog_copy.json" - client_container_config: - client_container_dockerfile_path: "integration_tests/Dockerfile" - secrets_path: "secrets/cat-config.json" - setup_command: - - "python" - - "./hook.py" - - "setup" - teardown_command: - - "python" - - "./hook.py" - - "teardown" - between_syncs_command: - - "python" - - "./hook.py" - - "insert" - future_state: - future_state_path: "integration_tests/temp/abnormal_state_copy.json" - - config_path: "integration_tests/temp/config_cdc_active.json" - configured_catalog_path: "integration_tests/temp/incremental_configured_catalog_copy.json" - client_container_config: - client_container_dockerfile_path: "integration_tests/Dockerfile" - secrets_path: "secrets/cat-config-cdc.json" - setup_command: - - "python" - - "./hook.py" - - "setup_cdc" - between_syncs_command: - - "python" - - "./hook.py" - - "insert" - teardown_command: - - "python" - - "./hook.py" - - "teardown" - future_state: - bypass_reason: "CDC does not have a future state as LSN will be absent from DB, triggering a full refresh" diff --git a/airbyte-integrations/connectors/source-mysql/build.gradle b/airbyte-integrations/connectors/source-mysql/build.gradle index 3cdd608a6048..a11a6e59f352 100644 --- a/airbyte-integrations/connectors/source-mysql/build.gradle +++ b/airbyte-integrations/connectors/source-mysql/build.gradle @@ -1,54 +1,23 @@ -import org.jsonschema2pojo.SourceType - plugins { - id 'airbyte-java-connector' - id 'org.jsonschema2pojo' version '1.2.1' -} - -airbyteJavaConnector { - cdkVersionRequired = '0.45.1' - features = ['db-sources'] - useLocalCdk = false + id 'airbyte-bulk-connector' } -java { - compileJava { - options.compilerArgs += "-Xlint:-try,-rawtypes" - } +application { + mainClass = 'io.airbyte.integrations.source.mysql.MysqlSource' } -application { - mainClass = 'io.airbyte.integrations.source.mysql.MySqlSource' - applicationDefaultJvmArgs = ['-XX:+ExitOnOutOfMemoryError', '-XX:MaxRAMPercentage=75.0'] +airbyteBulkConnector { + core = 'extract' + toolkits = ['extract-jdbc', 'extract-cdc'] + cdk = 'local' } dependencies { implementation 'mysql:mysql-connector-java:8.0.30' - implementation 'io.debezium:debezium-embedded:2.7.1.Final' - implementation 'io.debezium:debezium-connector-mysql:2.7.1.Final' - - testFixturesImplementation 'org.testcontainers:mysql:1.19.0' - - testImplementation 'org.hamcrest:hamcrest-all:1.3' - testImplementation 'org.testcontainers:mysql:1.19.0' -} - -jsonSchema2Pojo { - sourceType = SourceType.YAMLSCHEMA - source = files("${sourceSets.main.output.resourcesDir}/internal_models") - targetDirectory = new File(project.buildDir, 'generated/src/gen/java/') - removeOldOutput = true - - targetPackage = 'io.airbyte.integrations.source.mysql.internal.models' - - useLongIntegers = true - generateBuilders = true - includeConstructors = false - includeSetters = true -} + implementation 'org.codehaus.plexus:plexus-utils:4.0.0' + implementation 'io.debezium:debezium-connector-mysql' -compileKotlin { - dependsOn { - generateJsonSchema2Pojo - } + testImplementation platform('org.testcontainers:testcontainers-bom:1.20.2') + testImplementation 'org.testcontainers:mysql' + testImplementation("io.mockk:mockk:1.12.0") } diff --git a/airbyte-integrations/connectors/source-mysql/gradle.properties b/airbyte-integrations/connectors/source-mysql/gradle.properties index 8ef098d20b92..04dcba8cefd7 100644 --- a/airbyte-integrations/connectors/source-mysql/gradle.properties +++ b/airbyte-integrations/connectors/source-mysql/gradle.properties @@ -1 +1,2 @@ -testExecutionConcurrency=-1 \ No newline at end of file +testExecutionConcurrency=1 +JunitMethodExecutionTimeout=5m \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/Dockerfile b/airbyte-integrations/connectors/source-mysql/integration_tests/Dockerfile deleted file mode 100644 index 31a7e649b4f5..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# Use a base image with MySQL client -FROM python:3.9 - -WORKDIR /usr/src/app - -# Copy the script to the container and install the requirements. -COPY seed/requirements.txt . -RUN pip install --no-cache-dir -r ./requirements.txt -COPY seed/hook.py . - -# Give execution rights to the script and pre-generate all scripts. -RUN chmod +x ./hook.py -RUN python ./hook.py prepare \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/abnormal_state_template.json b/airbyte-integrations/connectors/source-mysql/integration_tests/abnormal_state_template.json deleted file mode 100644 index a17c2c6041c8..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/abnormal_state_template.json +++ /dev/null @@ -1,20 +0,0 @@ -[ - { - "type": "STREAM", - "stream": { - "stream_state": { - "version": 2, - "state_type": "cursor_based", - "stream_name": "id_and_name_cat", - "cursor_field": ["id"], - "cursor": "6", - "cursor_record_count": 1, - "stream_namespace": "%s" - }, - "stream_descriptor": { - "name": "id_and_name_cat", - "namespace": "%s" - } - } - } -] diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-mysql/integration_tests/acceptance.py deleted file mode 100644 index 9e6409236281..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/acceptance.py +++ /dev/null @@ -1,16 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - - -import pytest - -pytest_plugins = ("connector_acceptance_test.plugin",) - - -@pytest.fixture(scope="session", autouse=True) -def connector_setup(): - """This fixture is a placeholder for external resources that acceptance test might require.""" - # TODO: setup test dependencies if needed. otherwise remove the TODO comments - yield - # TODO: clean up test dependencies diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/configured_catalog_template.json b/airbyte-integrations/connectors/source-mysql/integration_tests/configured_catalog_template.json deleted file mode 100644 index a1cf9de07799..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/configured_catalog_template.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "streams": [ - { - "stream": { - "name": "id_and_name_cat", - "json_schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "string" - } - } - }, - "supported_sync_modes": ["full_refresh", "incremental"], - "default_cursor_field": [], - "source_defined_primary_key": [], - "namespace": "%s" - }, - "sync_mode": "full_refresh", - "destination_sync_mode": "append", - "cursor_field": ["id"], - "user_defined_primary_key": ["id"] - } - ] -} diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/expected_records.txt b/airbyte-integrations/connectors/source-mysql/integration_tests/expected_records.txt deleted file mode 100644 index 8c60e5d1ee18..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/expected_records.txt +++ /dev/null @@ -1,3 +0,0 @@ -{"stream": "id_and_name_cat", "data": {"id": "1", "name": "one"}, "emitted_at": 999999} -{"stream": "id_and_name_cat", "data": {"id": "2", "name": "two"}, "emitted_at": 999999} -{"stream": "id_and_name_cat", "data": {"id": "3", "name": "three"}, "emitted_at": 999999} \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/incremental_configured_catalog_template.json b/airbyte-integrations/connectors/source-mysql/integration_tests/incremental_configured_catalog_template.json deleted file mode 100644 index 90ee287fc467..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/incremental_configured_catalog_template.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "streams": [ - { - "stream": { - "name": "id_and_name_cat", - "json_schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "id": { - "type": "string" - } - } - }, - "supported_sync_modes": ["full_refresh", "incremental"], - "default_cursor_field": [], - "source_defined_primary_key": [], - "namespace": "%s" - }, - "sync_mode": "incremental", - "destination_sync_mode": "append", - "cursor_field": ["id"], - "user_defined_primary_key": ["id"] - } - ] -} diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/basic.sql b/airbyte-integrations/connectors/source-mysql/integration_tests/seed/basic.sql deleted file mode 100644 index 17be356eebd3..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/basic.sql +++ /dev/null @@ -1,153 +0,0 @@ -CREATE - DATABASE MYSQL_BASIC; - -USE MYSQL_BASIC; -SET -@@sql_mode = ''; - -CREATE - TABLE - test.TEST_DATASET( - id INTEGER PRIMARY KEY, - test_column_1 bit, - test_column_10 SMALLINT, - test_column_12 SMALLINT unsigned, - test_column_13 mediumint, - test_column_15 INT, - test_column_16 INT unsigned, - test_column_18 BIGINT, - test_column_19 FLOAT, - test_column_2 bit(1), - test_column_20 DOUBLE, - test_column_21 DECIMAL( - 10, - 3 - ), - test_column_22 DECIMAL( - 19, - 2 - ), - test_column_24 DATE, - test_column_25 datetime NOT NULL DEFAULT now(), - test_column_26 datetime, - test_column_27 TIMESTAMP, - test_column_29 TIME, - test_column_3 bit(7), - test_column_30 YEAR, - test_column_31 VARCHAR(63), - test_column_4 tinyint, - test_column_5 tinyint(1), - test_column_6 tinyint(1) unsigned, - test_column_7 tinyint(2), - test_column_8 BOOL, - test_column_9 BOOLEAN - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 1, - 1, - - 32768, - 0, - - 8388608, - - 2147483648, - 3428724653, - 9223372036854775807, - 10.5, - 1, - POWER( 10, 308 ), - 0.188, - 1700000.01, - '1999-01-08', - '2005-10-10 23:22:21', - '2005-10-10 23:22:21', - '2021-01-00', - '-22:59:59', - b'1000001', - '1997', - 'Airbyte', - - 128, - 1, - 0, - - 128, - 1, - 1 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 2, - 0, - 32767, - 65535, - 8388607, - 2147483647, - 3428724653, - 9223372036854775807, - 10.5, - 0, - 1 / POWER( 10, 45 ), - 0.188, - 1700000.01, - '2021-01-01', - '2013-09-05T10:10:02', - '2013-09-05T10:10:02', - '2021-00-00', - '23:59:59', - b'1000001', - '0', - '!"#$%&\'()*+, - -./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 127, 0, 1, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (3, 0, 32767, 65535, 8388607, 2147483647, 3428724653, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, ' 2021 - 01 - 01 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2013 - 09 - 06 T10:10:02 ', ' 0000 - 00 - 00 ', ' 00:00:00 ', b' 1000001 ', ' 50 ', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 127, 0, 2, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (4, 0, 32767, 65535, 8388607, 2147483647, 3428724653, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, '2021-01-01', '2013-09-06T10:10:02', '2013-09-06T10:10:02', '2022-08-09T10:17:16.161342Z', '00:00:00', b'1000001', '70', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -127, -0, -3, -127, -0, -0 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 5, - 0, - 32767, - 65535, - 8388607, - 2147483647, - 3428724653, - 9223372036854775807, - 10.5, - 0, - 10.5, - 0.188, - 1700000.01, - '2021-01-01', - '2013-09-06T10:10:02', - '2013-09-06T10:10:02', - '2022-08-09T10:17:16.161342Z', - '00:00:00', - b'1000001', - '80', - '!"#$%&\'()*+, - -./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 127, 0, 3, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (6, 0, 32767, 65535, 8388607, 2147483647, 3428724653, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, ' 2021 - 01 - 01 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2022 - 08 - 09 T10:17:16.161342 Z', ' 00:00:00 ', b' 1000001 ', ' 99 ', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 127, 0, 3, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (7, 0, 32767, 65535, 8388607, 2147483647, 3428724653, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, '2021-01-01', '2013-09-06T10:10:02', '2013-09-06T10:10:02', '2022-08-09T10:17:16.161342Z', '00:00:00', b'1000001', '99', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -127, -0, -3, -127, -0, -0 - ); diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full.sql b/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full.sql deleted file mode 100644 index a6499c150184..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full.sql +++ /dev/null @@ -1,528 +0,0 @@ -CREATE - DATABASE MYSQL_FULL; - -USE MYSQL_FULL; -SET -@@sql_mode = ''; - -CREATE - TABLE - test.TEST_DATASET( - id INTEGER PRIMARY KEY, - test_column_1 bit, - test_column_10 SMALLINT, - test_column_11 SMALLINT zerofill, - test_column_12 SMALLINT unsigned, - test_column_13 mediumint, - test_column_14 mediumint zerofill, - test_column_15 INT, - test_column_16 INT unsigned, - test_column_17 INT zerofill, - test_column_18 BIGINT, - test_column_19 FLOAT, - test_column_2 bit(1), - test_column_20 DOUBLE, - test_column_21 DECIMAL( - 10, - 3 - ), - test_column_22 DECIMAL( - 19, - 2 - ), - test_column_23 DATE NOT NULL DEFAULT '0000-00-00', - test_column_24 DATE, - test_column_25 datetime NOT NULL DEFAULT now(), - test_column_26 datetime, - test_column_27 TIMESTAMP, - test_column_28 TIME NOT NULL DEFAULT '00:00:00', - test_column_29 TIME, - test_column_3 bit(7), - test_column_30 YEAR, - test_column_31 VARCHAR(63), - test_column_32 VARCHAR(63) CHARACTER - SET - utf16, - test_column_33 VARCHAR(63) CHARACTER - SET - cp1251, - test_column_34 VARCHAR(7) CHARACTER - SET - BINARY, - test_column_35 CHAR(63), - test_column_36 CHAR(63) CHARACTER - SET - utf16, - test_column_37 CHAR(63) CHARACTER - SET - cp1251, - test_column_38 CHAR(7) CHARACTER - SET - BINARY, - test_column_39 BLOB, - test_column_4 tinyint, - test_column_40 TINYBLOB, - test_column_5 tinyint(1), - test_column_51 json, - test_column_52 ENUM( - 'xs', - 's', - 'm', - 'l', - 'xl' - ), - test_column_53 - SET - ( - 'xs', - 's', - 'm', - 'l', - 'xl' - ), - test_column_6 tinyint(1) unsigned, - test_column_7 tinyint(2), - test_column_8 BOOL, - test_column_9 BOOLEAN - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 1, - NULL, - NULL, - 1, - NULL, - NULL, - 1, - NULL, - 3428724653, - 1, - NULL, - NULL, - NULL, - NULL, - 0.188, - 1700000.01, - '1999-01-08', - '1999-01-08', - '2005-10-10 23:22:21', - '2005-10-10 23:22:21', - NULL, - '-22:59:59', - '-22:59:59', - NULL, - NULL, - NULL, - 0 xfffd, - 'тест', - NULL, - NULL, - 0 xfffd, - 'тест', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - concat( - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 261568, - '0' - ) - ), - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 2, - 1, - - 32768, - NULL, - 0, - - 8388608, - NULL, - - 2147483648, - NULL, - NULL, - 9223372036854775807, - 10.5, - 1, - POWER( 10, 308 ), - NULL, - NULL, - '2021-01-01', - '2021-01-01', - '2013-09-05T10:10:02', - '2013-09-05T10:10:02', - '2021-01-00', - '23:59:59', - '23:59:59', - b'1000001', - '1997', - 'Airbyte', - NULL, - NULL, - 'Airbyte', - 'Airbyte', - NULL, - NULL, - 'Airbyte', - 'Airbyte', - - 128, - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'test', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 1, - 'test', - '{"a": 10, "b": 15}', - 'xs', - 'xs,s', - 0, - - 128, - 1, - 1 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 3, - 0, - 32767, - NULL, - 65535, - 8388607, - NULL, - 2147483647, - NULL, - NULL, - NULL, - NULL, - 0, - 1 / POWER( 10, 45 ), - NULL, - NULL, - NULL, - NULL, - '2013-09-06T10:10:02', - '2013-09-06T10:10:02', - '2021-00-00', - '00:00:00', - '00:00:00', - NULL, - '0', - '!"#$%&''()*+,-./:;<=>?\@[\]^_\`{|}~', - NULL, - NULL, - NULL, - '!"#$%&''()*+,-./:;<=>?\@[\]^_\`{|}~', - NULL, - NULL, - NULL, - NULL, - 127, - NULL, - NULL, - NULL, - NULL, - NULL, - 'тест', - 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', - 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', - 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', - 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', - 0, - NULL, - '{"fóo": "bär"}', - 'm', - 'm,xl', - 1, - 127, - 0, - 0 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 4, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - 10.5, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - '0000-00-00', - NULL, - NULL, - NULL, - '50', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - FROM_BASE64('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'), - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - '{"春江潮水连海平": "海上明月共潮生"}', - NULL, - NULL, - 2, - NULL, - NULL, - NULL - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 5, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - '2022-08-09T10:17:16.161342Z', - NULL, - NULL, - NULL, - '70', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - 3, - NULL, - NULL, - NULL - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 6, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - '80', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 7, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - '99', - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - ); diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full_without_nulls.sql b/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full_without_nulls.sql deleted file mode 100644 index b4d6ff698342..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/full_without_nulls.sql +++ /dev/null @@ -1,306 +0,0 @@ -CREATE - DATABASE MYSQL_FULL_NN; - -USE MYSQL_FULL_NN; -SET -@@sql_mode = ''; - -CREATE - TABLE - test.TEST_DATASET( - id INTEGER PRIMARY KEY, - test_column_1 bit, - test_column_10 SMALLINT, - test_column_11 SMALLINT zerofill, - test_column_12 SMALLINT unsigned, - test_column_13 mediumint, - test_column_14 mediumint zerofill, - test_column_15 INT, - test_column_16 INT unsigned, - test_column_17 INT zerofill, - test_column_18 BIGINT, - test_column_19 FLOAT, - test_column_2 bit(1), - test_column_20 DOUBLE, - test_column_21 DECIMAL( - 10, - 3 - ), - test_column_22 DECIMAL( - 19, - 2 - ), - test_column_23 DATE NOT NULL DEFAULT '0000-00-00', - test_column_24 DATE, - test_column_25 datetime NOT NULL DEFAULT now(), - test_column_26 datetime, - test_column_27 TIMESTAMP, - test_column_28 TIME NOT NULL DEFAULT '00:00:00', - test_column_29 TIME, - test_column_3 bit(7), - test_column_30 YEAR, - test_column_31 VARCHAR(63), - test_column_32 VARCHAR(63) CHARACTER - SET - utf16, - test_column_33 VARCHAR(63) CHARACTER - SET - cp1251, - test_column_34 VARCHAR(7) CHARACTER - SET - BINARY, - test_column_35 CHAR(63), - test_column_36 CHAR(63) CHARACTER - SET - utf16, - test_column_37 CHAR(63) CHARACTER - SET - cp1251, - test_column_38 CHAR(7) CHARACTER - SET - BINARY, - test_column_39 BLOB, - test_column_4 tinyint, - test_column_40 TINYBLOB, - test_column_5 tinyint(1), - test_column_51 json, - test_column_52 ENUM( - 'xs', - 's', - 'm', - 'l', - 'xl' - ), - test_column_53 - SET - ( - 'xs', - 's', - 'm', - 'l', - 'xl' - ), - test_column_6 tinyint(1) unsigned, - test_column_7 tinyint(2), - test_column_8 BOOL, - test_column_9 BOOLEAN - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 1, - 1, - - 32768, - 1, - 0, - - 8388608, - 1, - - 2147483648, - 3428724653, - 1, - 9223372036854775807, - 10.5, - 1, - POWER( 10, 308 ), - 0.188, - 1700000.01, - '1999-01-08', - '1999-01-08', - '2005-10-10 23:22:21', - '2005-10-10 23:22:21', - '2021-01-00', - '-22:59:59', - '-22:59:59', - b'1000001', - '1997', - 'Airbyte', - 0 xfffd, - 'тест', - 'Airbyte', - 'Airbyte', - 0 xfffd, - 'тест', - 'Airbyte', - 'Airbyte', - - 128, - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'test', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 'Airbyte', - 1, - concat( - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 262144, - '0' - ), - lpad( - '0', - 261568, - '0' - ) - ), - '{"a": 10, "b": 15}', - 'xs', - 'xs,s', - 0, - - 128, - 1, - 1 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 2, - 0, - 32767, - 1, - 65535, - 8388607, - 1, - 2147483647, - 3428724653, - 1, - 9223372036854775807, - 10.5, - 0, - 1 / POWER( 10, 45 ), - 0.188, - 1700000.01, - '2021-01-01', - '2021-01-01', - '2013-09-05T10:10:02', - '2013-09-05T10:10:02', - '2021-00-00', - '23:59:59', - '23:59:59', - b'1000001', - '0', - '!"#$%&\'()*+, - -./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 0xfffd, ' тест', ' Airbyte', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 0xfffd, 'тест', 'Airbyte', 'Airbyte', 127, 'Airbyte', 'Airbyte', 'Airbyte', 'Airbyte', 'Airbyte', 'тест', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 0, 'test', '{" fóo": " bär"}', 'm', 'm,xl', 1, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (3, 0, 32767, 1, 65535, 8388607, 1, 2147483647, 3428724653, 1, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, '2021-01-01', '2021-01-01', '2013-09-06T10:10:02', '2013-09-06T10:10:02', '0000-00-00', '00:00:00', '00:00:00', b'1000001', '50', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -0 xfffd, -'тест', -'Airbyte', -'!"#$%&\'()*+, --./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 0xfffd, ' тест', ' Airbyte', ' Airbyte', 127, ' Airbyte', ' Airbyte', ' Airbyte', ' Airbyte', ' Airbyte', FROM_BASE64(' iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAEIPSURBVHgB7b0JnFxXeSf6v3vtVV29791q7bIkS8iy5U3IC2DHNnIAJ2MYQl4AZ4HJQB6TmcybQQz5ZfHkDXmQkEcCgQDBwWb1IrDBu4wX7Vu31GpJve / Vta93e985996qlu1k3vzUJiq / d + xS13LrVtX9zrf9 / 9 / 5 jmDbtgTAQl2Mzwn83zc8vfzBPn777Gc / az / y8qd9iaAULvoSoQYEVdGW1FLFtCuo6AFR0EoFcWkhX1xoX6uK5YnSr5fE4v7PXPPI3Oc + B + ENH / 1 Z79z / a + Oz / 8 KjX8IQBBKwQMPG22Q8e / Hrvuni + e6Z / MjWRD6xOVcsblRDaJMEKeRXA2pZL6NoFso + ySc3CO1TF3MjL / cG + 62 oHJCazYYH77v + C0W8TQbJVqxbAbvfm9 / 1 nvv2kf / cO5ub + r2hpeO3SIF8d3bJipiirQX8klAsmYhFJdJdoFQxIKoG + qUd9lx5utTh7zRHkxdHFb / wj / F4 / JsP3vizabwNRl0L + OsX9 / mkhH63rAVeKKbThaHcqVv8duDP5tPT61PSFIKKhsSChfYOGZm0iWIZdF9EuWwjlbIQiZsIlltwXWQt5spFfH / weciajcbG8NnmePw / xiK + px + 88 aUs6ngwAYuo0 / GRvn1l0YRhl / MfP7z48mdGps9 + KSI1rNfJw8ZCKpKLJjRVhKaJKOVtiORWRUGERDejIsCqkDbLSxg ++ QouXDyE5rYgZNOH5FJh3cTs9JfG5xY +/ fs / v7YVdT7qVsBkne2AobxUKVU + nEjPf6ot3NbdqDXBktIwSFuNiohgQIRtAqYuQBIFLmSZ5rRgC6gUBWhBCSdVHRdME9GwhEhMhmgpqJTQNbew9AcLudQff / LcJzXU77DrVsD / 49 RvxZ9KPfFHo3Pj / bd0vyd0 + 8 CdmEmPQdJ0FAqOIMn3ks8VINB9SWIazAQtck02dHqORB5t89F7NHosoKlJhk + TIQsyFFMNT4zNfzRx / Mm /+ E8 / v7URdTrqUsD7Satmpi7 + 2 sTs5Icmkxel7koRPUYOaWMMgqTCIuExASuyCL3CfqTIBUui5pos0V / bEkCKS5OAjiWB6yWaFAppcViGYJL2B2Vogg9zi6n7p3ILv / bFc3fUpSbLqMPx2tjYuuml2Y9WimK8pCxg7MgzSPiDKHUWYCsKLNMm4Qpca5kQBf4fuGlmkbdIQrbovmGQgDUJqmLRRLDofQLCIYkHYYJoIxwRkMyUGtLZ1EeHRuQDdIoTqLNRdxr88MMfkJbKyX +/ mExu7wy3CLYUwvdm0vju2DiskEzaS8IzBKgq99P0GJegFlzQgiNsg46TScs1P2k03SffyzVaIW1nViBA51MFhVKs4rZ8qfzpD9gfkFBno + 4E PNPZuGEhO / 1e kBk1pQLWxq + CNtCI8NY4FEWGadiUHgASaTBlgKSpcCXK3i04Nxv8GJM0XaL /+ iK9dJzlmHOJRd5M4CIiShQ + OqdgyEgVU / dseq6wAXU26krAtr1PHE8Nvz + XL8RVUUPJKqKk69i16h0IR8kcWxKa1Q4IFvlV2RUqEzBJ2WaStmuoiM3vCCgZJTSHmtAR6kWhrPO4U1UF + IQgOoJ9kFUToqnSxKk0jGem3s8sCOpo1JWA /+ TFc + unc6N7YSiUz5IQpABOz59El7oKm1p2kHBU9Ac3U0RsgWX4lOg7grRdbSaNtZiwXZttsRSK / O5w + hRu7L4FQTFKQIhF + bOAgdha0mgy1z5HyyWTgJP8wr0nmzLrUUejbgS8b98 + cWppYpuulzuhUypDAZRBwtINHacWjmKTdguujl8Pnx1BhaBIFmBxSXKttbmwmW9m0bMjdGcCEAGBVDmBUjmHa9puQJne61f9GGhch / nkPFTZxyNvwST / blodk + mFbey7oE5G3XzR7l8vBHPl9LWkdTEWGbNImEfIZI5Hc8MYnziNO / x90IqTBG5QFOz6WNs1ylybBfD3sKcsemzQcRbxaJrow7GZw9gavwZhqREdsQ60BtuRyCTJUkg83bINiVIOMVYs5q7133YkiDoZdZMmnZsYaa6US9vMsihblN4ITMAWS38oy6WI + fjCY4icUTFMM8Ai6TJBsuNIhnRjYha4MJnkTQqoVMpxo4qfhJyFT1YxsjiM + cQc3tX + a7A1C + n5DArFHILwQaUMuJRh75FlA8bVk5lcC52pLnDqutHgXCbZXjb1XstyzC4LjG2XxVboQTHmw / esLA6WS8uDZRK0ibZQuxNvuT7ZMA2sb7qK3qdApwnBoA9m6l8ZewUdUhu2aHHMpYd5rsysgMzUgO6LFICZhtGXz6XbUSejbgRs2 + Y6EkKzaYluLuv6UvoFpMykxZS79kYgNwYpRRId6dKtTMltR6iHTG4XSLdJmw00BVqxunE9JrMTXLgEZJGflXBudhCzg99A6eLjkDPDzEzwScRSLvYZLFc2TLPJFsy6CbTqRsBls7LVNHSfTZmMIpJKSY6PZZrMI2YBDgxJwZWkOpPAoNdFst8XE6PY0bKH + 1 pm1vf03Yn5zAwFVhUedLGZwHLonF3AM + dO4NDRQxim1yVJ4lrMcWz6DF1nGm35KqaxBXUy6kPAJEfdNPqYUrK8tyfWSxBjkAdKXLKuNnMTDF6nwn8Ze45FyfPZWQSFCK5qvh5doTVYE9mIwZlBsFya5cfMk8s0YRSfgsG4D8 + 1 KFiKyPziML8tkAYrCglXdxLpimH0A29S1nMFjroQ8Bd / 8 km1WMr1MhDCpCuuyQG0 + LsoWrb5ZWYC5RbZFbJ36W0X2SgSRHVubhC7YjtxT +/ dWMokMM00lCyB5fpzhmApqgR / RIHcqkH1Oabb8fekxbITgTONLxvlno88 + 5 G6IB / qQ4NDoUjBKLUw2SmChmRhAX2BDRQQy9w3Mg00bU / ADqEgeCgWDYmkc3HmWRhDj6Bp9hiOjnyPgiudR + B2VYAi97UsOmePWZ7tBHJOxCYrDkHB + OWyXm5pj + bDqINRF2mSikozYUlRJpKyUUHOSCKuNhEs2YnpwggJxVcNqrjGcWBDqGoz86Epo4zXBk + iOTyMEQq0JGaTBds9hgRMAhQMoYpuitIy00 // kTvmxzKSgiLpSChXaabDFnCFj7oQ8FIl0RhVor5NTVuRD5tYKJ9HhXDoTS3bcHFkiI7wuWiVIwzBkQ3XalYQzIj + ok / GqxoFU5KBBEGQErFF / D2We7zo5lZM + 1 lULdUidcudKJ6ACSDxpXKZZtTBqAsBL + Tn4xO5cbFIZMA1TTfhzoE70YgZ9GpLeIHSJoNBjlYtL7axrIZQcLRSIh8rxxT6xaStCzoHQNiRpu0d5ppkOKZekpY9ZjZBqPl327LEpXKmAXUw6kLAAdUfYFUYS6UkHh16FBdmT2EHZbVILRBAYTooFWpWmmue4DJGEr0qOVEX11HRI / 3 dZ5gph1DVevYUO5fInbtHPzkns90PsemOYhNIXQejLoIsyxRjzExKFnGzRAnO2GN4sjSJR5ZyKBHMJLjpkWXVTK5DIjkskjMurQx2BOpMBCZiLtDqrOAYB2pxms35YnZjE8Qg9Ktimqu + cujjCq7wURcaXCBzKAquFgqsWkOBSTpdIdpQEfQqW2QvW4DD7jv + 2 PGp3l + XPXQKAfhxbu4MR6jV2eHhnQDHrttCHUgpCRSsRbRF2jHQMBCbyQ5f8fXk9aDBQkUwNEdKIv / GAg + AGU8LBl9hWeL7etlUzS7XVNHRbp5Y0R0H1nYnjlQ9hYedVKtAdIq6G / wxbGjehLAvirvXvx8lwyjue + dzJq7wseICvmhf9Nk1u3jZ42H7YdHUjbBnUr2aKsffuigF4Aq3pn38K7D6ZyIRdNtwyu74oRbPZdc3beC + lP3HymqdhFpAdY2Hd0dwrMBsbg6bWrfhroH7UdEtHJl9LbLvuXde8dUdKy7gzNx4 + 9 DMgb2zs7MrwpkOPvKIVDFLmuBKlftVuKAGP6L2L / fBtlDVPpHUcnXDGvRFVpGiqyBRM7IAmWIWzYFObG / bRdBnCc7U8KJm582iW8fF4zRCvOZz82jLDOKGiB8 / Hf4R + 3 zf6YX5K94CrvgX1FuDk5VysUUXFt9DmqwNzTzbh8sY8VCbQEGN5GivUI2VLGuZOYaHYqEW + dLxBmnuTHYGcV8jbu29Czd23IMmfw8v1TkzPYRb +/ YiojVz + FN4U5MuVIMxdufw0PN44vi3sFCcpbRMltpClRWzVG / VWPEga4ewQ3 / 21 LeeVqF8NFkeb64oha0 / vLDv2YpVqJALtSq6oKtiMKWa2tixdcXZfcK + f3Ft8pI / KxAEfUm0aguUpkgqYmoIGXuharMFr0RHsFxEi3Bos4ij0wcxlj6DXe034v6BTxAmPYXB6aPoJvp + V + MAThunavCml / l6YbQ7p1RNxpOk7VpmETalXoZtKll / L706git5rHwUTdfm7x / 6 Qk + j1LznzvZd1xRDF4TX8qd + m / kt5uaIiaHgyG / 4 NP98KBv74dMXvvZoj9n +/ Jo1d5bf7HQRLU9MEs0JTvoyzJnyXpJ4WI0gGG7C4tIkCSPgfHS12Aq1AmgeDkhIG2k8v / Q9nFh8Adc27sVdLZthj / 8 Y15dnMEHRVp4Fa2xiCF7k7dw80ISHckEJop + AEp2lSoayOJy94k30SgtY + OA3tl87m57 + qwVpdoM2fR5NQRVGj48D + xXD5pFwNluRm + PrOnQ787GF0Gg4mt / 67 D93wsxiUTBtQ2SXuSXYDMUoUgS7QLRfFLIVgkcecUiRf4NloIbgEg / 0 bpmwx4ZwjFKuLL554u / xC6IP90Z0zJEACx0qLllnadeAjyoAIgq10mqR5eamhM4rfxnxigr4o9 + 5 dvP56bE / t0VjQ0tzBBm / QsSAjLCkcD5VrlgMtkdFLWGV0AJVGVAPzO9 / r92p / 4 je / sM3Oye7hJZg8fA2S8K5rucmjFpH4FPCEMqaK1yhFk3Yy6BKHmQT + S + 6 kTIdFgz64fNZGM5l8ZWszc1tv + ScR / QyLvcvnxySd39Znizwgvor3v + ysSIm5vGxLzf8 + dO / tXNycfqPBVG / qa2hCbGoD6YgIxLT3HSGKAEf4cYkq4bGIE6ffhzHzvwY87Pp6NHFn3 /+ W2f + 4 zVvll7dtmqHqEk + mb2SLmfw3MhzuLX5HtxMOSmMPBfgcuzZ9vJX70zM5FYF5kCQql + FP6DBChH3G9Uga5JTICDWrghHubx0a1ng5f0WBn5XUtYVL + QVEfAaeQNRqdrnTal4y0DLgNAajfGVBSZf + 0 NAASH6rLJRJFOpE8rvJ + 51 oknBfMSEavqQSWdXn0sd / rUfn30w9Ppzx + 3 OoCr5Gi2KdFmpDsOjXzz9AwgXfoEyYdIQnfjLsm039629t / awJgfmV2Ui9hW26IwYJo1ubFVhNUpnuDbDmimt6gp3UWJlOni1wIrvZLRH2vgicmsFc / 23 cqyIgGPBrmhMiV + 3 rm1tcFvbtcjbCU7GM7JcIBPIAizdsHiNlE73TRJyIKJBIsiRr8DPUXBaTL77TPrkqtef26eEpJgck1kNM3sfu / AT1jweunAaJ1IZXlEpkm31SwGaPAZ / TxWd4jehWoXJgQxWX8VuLsHPiH5Pwz1wg93ViXdeHVuPgBTk582Xc9i9 + ha0R3sIONEdjqIOxmUL + OFT + 9 S / eeazv7060Bv5t2s + hemlObqA7AI59S5suUhFN2GQgCvkg8t04xE1aTOLqH1 + CeW8gEwhvcGolO72znt89sngoUOHlFCwRWj2tQpMg5kHZ2AGNAWzTUGUaZLwGilTR2ewGz0RuvhW2Y2mXR63KgbBBTOc6JgJWZBc4VYPcScGTZiSVUKpZGB7 + 7 XIlNK4pnM3dnTdhKHFk65LsKDGxLc / Fn1samjt0Mih22IzRzAxehhnFk8TvRfgzU4c8IFMoB3ktF6ZaTIJSndXHlR0MtsszKPwt1wypYyRvpmgSYn5YlnU1kpti / dKsBtjvmZLslWHzXENoyo6VZReffTw0llsbd / GiQCDBC5QdNSgNjoZk0v91aJq1Eh + Nx1iEVY1kILz3ERiFFuar8XG6A24a9XH8NS5HyNFLkKURNRHiHWZAmZrdC6On7tmdGGh96vP / ACPHH8IBsrcRDJhssGsZrPWwbWPpUmsMpGZW6YFTJN5XM0WalNumcovbtYuGk2s6082XThfrKTXpSrT / 6 VN8XVq0HgO7Gmkg0O4 + DH9jJJexrnFs7htzR0kMwqixCBWRda7KbEbJS0TilCNlr1zOPcdxpBZGBnT6Sn00ON / t + 2 jWMzO4eDkS + QiFO8LWJjBFT8uS8DlG481LCYTewqmFTvZJmIpJIIZLbZyvlJxsMNsoYSo1EyBCUXTlDKxSoqKZXO6jvtkw + mAYxkSMuVMy3z6NF + De92aOzNlQf3S4viB2eTx73XLyTxdfKlamuMhTB6eoZJQT06fRClfxK9f9RF0 + Tbwmi2LJkWNRbKrsKPnbZ2Hzpn4Kgb6T2PltDSDysUF6GPfgaK / ip9d + BvKCpw2EDbnj2H6G6S3t4men5lrKJX11UwB / ORLmallmsXwDNNwNJhepwsloD + 0 BT3 + DfCCJdv1p0zIHKFik0KviFk90eKdf0 // vSndCP7t4UThUIJsg1QjjuBppHeFOfdLQd2z559GuxjBPZ07IOpJdyWhXaX + ariowLngZl8zBXIatzosmOr291OUH + ATwwj58Mj5YXz70MO4kBtjq / 2 rkbloi3XR / vGyBJwspZpK5UoHC1iclXu2uyjMFRwYNElmlczb3ubdmEkvIEAX30eRKTuWvcbWBrHByAP2WDClSzraDLZHz463 + gd1f43BYwIT3FjZkZ3TA4uZ1bnSIl45 / U00TByAMf8K / 4 lWtV5LqL6HSYkR + UE5hu7gGpTMAiFjGjFPa5EsLnFcWw1IOOgT8Hw5D4NpL / fj3moKQc / N + q54IV9ekGVgTblstPAVALazes9rfGLx5ic2D3ZGp4 / hyRcexFh2BG2 + XrT6u1CgC9oYaEV / cC2Z7IoDNVqsSL1wSb3x6cGHmboY7Ly1paBwIuplnC0DMFgts0a56mulBfz1mefxQi7JAzGvEseDGkWPVKAn54jn7fddw9cO9wZX0 / t9yJl5nnqxq6MQOKNqzn37Eo8u6KE29Yo30ZcFVZoVq4fSH02hC8CnsuWu27W9khmBR5yjGqVBUoHkRB7ODCCqBdCkdWF9fBfOFA6wJkduyY1NOLN + yYqBhlV + O380b1lV0NkhFRzuvloS6UbDIq / ysIj5Od / sp1SHXkpZ1fdU0yfHk3PAIl1KQM8b2BrZjcZoB1GBY5DJEjh + flm1CHAJQiba / x / IgyuG3kQCFllkyYANy3KCGNaOyAuA2D9aQIE / EmAr5MkEG1gX2kw + 8 jdwnqLeheI8xwi9VQkk5Uu + U0c2bEuiaGGZBrp0L5ZXc / DKSfchy2J8hFZJYg1mdKBGp85ZJexZoaCMN2IRTJxPnsD7V / 8 bbO7agvE0 + VpZdX + LXYviPKDE + 3 xJNBuS / rd5HixIDQzAYELtDnRWQQS + 4 Nqu5TPsOXbRGbLVlruAzYQpH55 + BUfmjkAyFUfjPc0Q5Ev82saFZpo6km55BtqqMUVerToP3VlttItSCV5OC6fo3XJ1lrdVog / bFL + Kt2ZgX1HTNFw0BoHCGCLCBRQqC9yiwJtwQg0NWz4kQdKTDave3j6YctqoSBchXy4gphHB4I86F5JrpF2rsoDjB9lykSOJaTx0 + Bs4nHoRiqw5DnGZdjIjvvwzBgc32Yqilt2X + bJR55y2m7 + 6 HtEFLoRlWkukPIJKCKsa + ilC1zmypQpBtAS6mK / nFKIsyTBUA8eP / l8YI / LDZOtTq6sU3S / huoZqWY / NQZbypsFH3uYabEPjESxd1FyhiN74KjSq7WjUWuni6vAyVWeJCAUsBD9Nh2UcDDpBEfOBPAiyHY0EX4urXkL8E5hi2QZK / Bq7FB4b1eJ270CpptHeMWR / sZCb50tcOiKrkNUz6PVTIEVCNuxKFfvQiGx4Qs / iH8ankePrRWvOVvQQL3cqVX86hQv79l35nfIvS8CO4RXgI02cWJjA1qZr8O6 + XydSvcBfXw4F83iIhOALEMpEPDE3oyJcSg7uMn1W86zpb / gcRTCWrQp1nvOiYaH2bTiR4LoDjx1ine8OT7yK96 +/ n9dj7Wy7hcxwmtdhcTfCIE / 6 FmY8iArlvZIkumnYcqRMcBml2vel8xuog3FZAib9sxwxSxSx5hDPXUSvlECimKYrITlNT + BWOdn2JasJ + PAqF5flt6IgvaHWWJFEY3nQ5pzLNcvLNM07DReO + 8 tYADieGsNschQf6f8QOtUGzOVnl388PMrXYZk8Hy46 / lxwKyy563E + j88LqT4EfFlpkiJJJQ / mkyg1OXD85xhJqDCDPt7eiKU9kofzehdHWJZNVoXNLqQTPcmi + gYNVmzV4GbeM71VYt4Ngqp0n / u6Vz / tCpmBoQdHH8ceeSd8kWGkc8MsmKue37aWR + ZOqscE2 + 7 vwLQ + RS9W6LfK2NS + DhfypzlWLdnS21 / AkqSkuBmkCySTf32mXEIzUX8NsQAYFM04YElwyH6 + ms + NUBhcKTBOEXZ1lZ9H5SiC / EYNVuU8 / zyBdbdja5Ec0yC6rBC3EIKDEnsm23YlJikMHxcwhxweO / szIvkpDjAplJOdz / fMh2OURRelEnknnpARxobmzTiTeQ13rPkATl64iFKlxIv + FE3JoQ7GZZloikqzXFAkJUach0IKAqTJ3nWzDMcJx8gs8ooLi1UjMtLe7 + LKQhXBcpqGMi194zYDqqwU2PX3qRo6Ix1VYr9qol0 / KQrVKKuqzXz1PvlVmybGiRYNh + N + 6 PSdRXt5tYfgfgHvSZE3QJvJzGBrw424vv1OdKjrcWz6NW7y3eLLMupgXJaANVFNy0TVsN5VDLFiQrYMZ40lJx1IwBbRg / 3 RAQL0A7yFUaPWRnjvAAladxZTk3SDxB / LrskUX2dVvvjKJyOKrDQTRQG / 5 qcUpxmGbjglrZ4QmeKKtY47ooSqv3fWMjmdcjSfwrvpiLWlD9V1v3wRWjUkt3nbh6XSElrp + 927 ai9eHX0O8WCELWV1UDBRvCwT / fFDH1f + 9 MX73 / I1xpcl4EAgNCXLss6YI8FyfKRecYMR1jafNNswKgiJjWgL9iGuteCWgdtxLjnCu8uxC5st5rEq3ofuSA + XSskoX7LkJU7UQ2e4s7M13IqwFkHYF6kFbEBt9QFvmGVfgnR5QTYTMOuU46VPHt3oAVWOFaHvzzy9FxlaTqB18dg / 4 MzxL2CqMIT1TVsIvavwDvL5YrGCyxjtxoRfV + fv2HforgDewnGZZIM0LctKngcprPuNopB2ma6AWaWrwp / P59PY2rgL9679MMaS57CYT / GotzdCxAORD4ocIs3s4ILJVZbiy5t9fmjNlzKJYurAzo6dhF2vc0ykx + dWAQhUm7Gw + zLRet7aIsezwrXCQjWy915womKbs1veC + z7M8TL79ewv5TFI3ND2N51I4qVMipmhft0glf / lwTMtgT48on7GzwYXZAlSsXFVQHLirDH9ltUhnt5ebCMeb + ipkl / USkaaA41oyvah0Ipj4iPEKTYKueCpAdxY4MKozKPw7OH6GiCJ4luSpbmCZfegj1tN6CLtJNpzVJxseeuj911yer5dClXmkyOYysF2EpmhFdSem6WV0fZNUDFJ / vRH + 5 zGSo3WXbKw5xo2eWHqyYctedd5 + Iea3EQxmzUEGzqx1VNu3Bx6RyPvtl5NVX5f2WiWX / pBwdv2p7XjX9vSuX / 9 tdn3 / e // 83 Q +/ 8 g5rd / x6 / 5 rrXN4OYvvHzvhgeP3vOWtEe8rCi6s6VvcXJ2cjaTy / QZJR1LwhLuXv2reHbph9jafDXlnlMctpwmjvbxn34JrxKGIShB7t + YNpXtIn545oeIZU8hY + dJ7D7M56c2LxaG ++ n0p73PiQYj5umJI / jZ7CimkgaUtiDXUObnw0oELcEo5o0E96e95O81csKsvEdwF / 06E KNHNbo9tZwHVZPOA0IWaLF2w0zgpu0K2sa1zbchkU5gIT + HYEhx0j / 5 jYDM68cXT + 4 dOG6c + 8 RUYu5 / W9vUH6kQsjaTP0eZgIgcxREd4gCCdvQaUxT2R0T8F7wF47I0uD3WkVckdY61BzQqFvnTFOYWF8gUfxAD4QECGKZoBklIxTR8O6MjzcthHFSoQn6bbYgR8wfx8 / I4XqX3SoaMvJFrPz738tVs1y7vc3xSwDDp0SHypRMBCsjcWirmI1nX9y7ilJuJYy4bBgZi65AsLBGvqy4DVxxtFUWhSht63QAEYZnQeTRv8TSJlX + xGCKstGFTbDuOTb3Cg0bTaVlLiLn4Ly7 +/ pOhvY0L1sSfBYSmB5r90UhTZSPidAuKFGwW / PCZYaiZJFpnkq0XRk / cniol34u3YFyWgDsVf17zaaN019CLzg8 / M / oy3tPQQb6yiAwFUOUCCdIvI9QcYkutyT9aXEsqZOBYKqoS4C8oPviCGvQy74MRH0 + P3PniyCPVyo6QGK5Q3mqpdB7GPQNV94uyWSK8eQLv6bmXC7qdiIQ0Rb + M0 / Uap9ieM4YLny6r8HBycdb6HTz9alKbEZGilMMb5HNLuL5xHfzkbudLM9jRvQsbm65CSA3aiiz / sz74i / vv0LL5id / J6vP3qKWo // bGj6E / tJ20tZEmtEYgiYIGvw8JmixDxjmYcrZrMTWy7 / mJr + 7E Co / LEvB7byjmGyKNxwi2y7B11CqZxoSVxo9 + 8 iAOj30PPjJnuZwJq2wgEJSJwWFVFxbHfvn2CGQ22f5ERZocskLvJ9DfzEOaz0 // yk9G9 +/ 1 PicUjZdIq8zlvK5BEvLTxGhUGwldGiYTOoIHb / oIhMoc8lahinPzH7ksomajBm3Y5BY0xKVWvhBcJhexrfE6pAlqLVF6F03quEddQJs4gd +++ pMUL6zGVGKSRf5mQPWV3uya7LP3ifPx3N0Vcel3hVxYnSMq8vzIEWylQBOLh8HairMKUkIBUSSTra2KIhwKUwRvNL948Vt /+ vSFb / eyc6xUl4TLC7KEfdaq1u4LPk1K89ortmohJONvzs / hdCKPcEQhTaDUJ68TSMFyUY0CsArlkiwtEZEtEVrkIy0z6ceWTDREibojnqJUKEVHFk7 + hwdf + Mx29jmqJfMtNRhcyKJoizX9JgF3RztJm0Jc4EfG9yN74XuYHP0pPTZrDJUXTXtSXkbwsbsG5earQ5s5jdhLAmSNwtN6GgJd / DS5lVcPDuK1l36Cbz77f + O7xx4ma7HI0iRLfRPEjY3gL843Fc3sr8qG0mJVFErtAjiROoz9I /+ I00vD3M3rvPjf5pgBWwyg030fxSbT87NXjyV + cfc7R / q3fuHlT69Im6bLLnzvaF97MRaKTjPzVskZBCbQzw / 4 SKgmfBpr7SuhkDdgl8vY2L4JIbmbfmCWm2bNbuBUfJwmRb5AP1i0SMhkqgmYzBbSq185 /+ QX / uDxD7wjFozlKLCxGLq0rXsrN6UM0OhvHECeLcqmc82T8P786Kt4ZmHWhSHZqIZT7hCqgZMTd4sos51bjBxu6bob13behInUKFkimaNz5aAfX01l8c3EEI6nTzslRywFo3ua + uYCzliJfsr9r9OLvNslxwVkYqlm + 31 Yiiq8TJhBuIWSw2alycKx51S6FrpuRVOl + Y8HhcjtXZO7LivP9sZlCzg02j29rmfts6ago0yCFEi7fGEVubRJwQSBHA0 + 0 k7S2HwFF9NnsLFlBzY33g6RfOe2tp0olYLw + YmY1xQsLNIEobS / Ma7R8SKSmfTNJ0ZPPPzcyecfkE2lnC5mkCql8L6rf53y5m60B7uRKae5uVOCKiYb2HIWv9tn0vl + QvVfwWlPCCda9gAOFtEfnTuMW9vWYWtLBGdmT / E2wwy8UVjH2a4wLII3FQra1jb2c19NCJyhioE3FYCuV24ulko9hbzlnN8MkhFRuNYyQeZLNl ++ ky1YPC1LZQyQasDKBSlVvFoKon1tQA5P33fffSvSweeyBcy + yIaujc8HAj6jVKHZmClDDRLgQd8 + NUf5MJldgTXbzpr0Ywt48eJTiFNk + qtrfxMbmjagOdiGxWwBDSRYH5nf + cUyIU86WlpV3vSskC + sOjp68BOlrBFULA2nZwYp2hbxgdXvIdNdIQ2sQHDXovgUZ79Crw1wbRWEI3AGYdrLYGcPFCkYRQy99iWcO / 4 Q68nFD3KreTlmzjQ + 6 mtAM6FpLPgC + 2 Z + 35 ti0cVycaNhWJKui2Bb3cblTr5tbZldG93RXjZ52GM22YpMKUgMFxKj2Brbgk69VxwcO9WJFRorsrqwqaH / YFtj6wVW41xYqvDoOBgIQ8 + SOTN0vpIwVyRTxdYiUUT63ePfgZzNYHe8jFVKCamCQCS8jqaYSZFwL / x6D9KFPDra / IgEGEBSUYl + lC1D5MtSXjqzH82Jw6hMPUdmXawiWE4xnFBtV1htyGLV + GPwVRW1kkgme5ny0q8Pnsc3Tp5iWzJxgVqmA3swJqxBimNb + zuc1Rj0GyVBNMPWG2lNtnFXvlzq0 + mV / tB6skpRyGR67YpaXYBn8r2bTLJcvK6YMgebFx0uUcaRzD + P2cH9yvn5Y +/ ACo0VEfB9Ox5Idzf1PqQEZCOT01Eiv9XR0IrVLRswM5pANEK + y6cimdJBd + Enc / r1I3 + Prz / 5 V5hauICA7KMfZxMVR5PAN49NrRtwa9dvIJcidiooYMtAH2l4ADqBKXq2gtlSGn839CoeHSdMW2FrhWyOQ1dX4PPhthS + 5 FfW9kyCWzrrdIUXkGmJIhsLccLEEbBzY / sohewY9vS8kyxNgiNczETbfv8bBPyZp877C + V8O3vP8NwwtvvejR2hjVBIiKWSY6KLrGN9eBVNWgktvh4YJYEvxpNJ4w8sjOGhc4cwkRzdfGjq0Ipg1CsiYDY0NfTTxlDjxb72AVTSpFV0cVa1rMLSUgF2sYwIQX4lSg / KBR0NGmlGKIYfkf8ZpZncqlm8D + USWcd0sYjTC08SL5zA7133R1gXvAXv6n0fOprjaGqmqDSo8H0XJsN + JEMB + CWVbn63rKbK / AOvWwwOd1WClwPbbnWmkyuzrj3OYvVqC2HLbedP51kqJJHPTlHckOMlwYTOGZKovQGqXNM44NdNPSLYEjKFHF4Y / jGGDn6fXNUkp0otvjzHoHMY2NJwAyJWF1m2LCy + WRcRM + RDpKYQoVyp5n5fWxwrMFZMwNubNp5RFOXpdL5ovGvLXSTEVrRSGkMOGOn5HPyUGqkUTabJFwtl8k0hiqL9AWQrjGIkHDtI2svy6DRpE5nzQ3NPYDr1Ah64 / ldxe / 8 WpGaKSKUqlDcLWNfWwzeNNAW2SHuABOyDZS2rf6vSSB575Arcc8wOn1t7aNeowhq75LZiosg9ayQxPfhdaIU0vcabdhhhFii8bmjIRwnRCzFBUiKFpJ3G94lcmWGd9ixnvRZbBjuWGkNzhlK55Az5aZsv1mPfSSczrpB / K5XzkenUaBtWYKyYgH / z3n2pmzZf / 43 ZzMLZV44exG39m7AmCjKtDUiSX85PphBtUCD5fJhbMiAVC2j0U6KvEf6csciEVYiooFw4oCCbFTG / pOKxocfx3Cv / HefPfI3AjgKlXWFMzZegp6NEKGzggc + q6GqKrDM80KoSEJYnKMuBFj1 + kGmRbbsL36rFHG4xgnOzvD7SrO2EYfOemMxnfu21szg4lySolZlwUT9y + uwbKipLphjSbVNx9sG0WdUHAs0ESYZUvlaLCZLt48SqRb577Ac4ljgOTaC00HSWxQbIX7PvWCjryoW5U01YgbFiAmZjx469x66 / ets / naBI9 + kDj2DsxENQNdJYVUNiQYe1ROBHTCUySEV6SUKTEEEDaa5GsF0yJ2BhiXw0kTSdjRL8NJMzOQ0Pj47iT08cgkmutrtJwKaBCLIYRa +/ H3vX7UWCzGeRomCxVoDlprsEHoga3 / zKdLV7uRK7pFL1L5aR / Q7Z4EwQm3cWINAjTumO6hTEU6hn + kOK / frfnytkw7ZpqiLXcieaZ5OHJ0QE7LDAjQufFTo0RiCQObZIa9lnsdrgJqWdofW88wGheSuy6ceKCvjONXeWb9103d + u6u0 + 8e TYRfyPA + dgMf8bU3jJzPxEGnoqjViTRpbbj7DcizwhXi0Btts2oVymjJlEBcVsiVISG90tCuWiGtIk3bklYGZRRyqtE7iRhZkbxC1hC + fnjvNVhU6ZkNviwXLua2S6r2reSuR8obp9ju0eV6vkpP8Nh / Bng88Bbk5tvrxFI / SNoWYa24vJ9c8EuJhEILxBgzOFjJ9t1RSUw2jzd / DWEhVL53XiIQ5kmHxZKovMne4EjpWw + cpKQvjEGE3KAAWblBuXzBWRzYoKmI0Pv3vf / KZ1Wz4VbGg8m5Nke3EyDztXgj9GWulrgr / YBoOE6AtI6GrsI59Mgp / MIlgpoCXCut8EyOTKmJgnQeeLaA4Y6CFTH / OzHUdVCtRkErKI16aG8JdPfhVzxQThyc5yh + VlrSw4ms3MI + iLYHPzduTyeSeq5qbS2aGy1sHdrpIQzrC5D95EWYAM1dnk0vZcOueYrXz5jeuSSkIlyNIoduy2zu18j6du4sSvjr0DE5kJXl4cFuOEw5OlUmMICTESusEnJE + hKO + Pa7303h2IBRqxEmPFBczG2sZdL28c2PgFSVXnGdyTmi1ALBIB76sQsN6IXb3vQn9gNWlpA1pbGwloIGHMlVGczyIqltEUloh39ZPp9WM + JRCiVaEou0QXp0zaYKAlSuZbVXFMDXBzD5cCNGsSc3wyCfTViwewu / dWshZNvCaMmW62l6FsK + 6 m0U465G0gzXL5iBil + KCNr9RIE40puBOGB10202zNiDQF3yDgkBKJ2PQZRb1In9eIq5tvxjWR3Tg4fJDn5uxzmtCLuK8P6xrWIpvLV9fasX2MUUni3vU7ETJaMJedvTKQrDcbn77v08VNa7Z85 / qrr / 8 LuqRm0WS7eRZhE3gxNH0Eh4aO4ta + jVgdTKNVKBOcqXH8mlw0FmgyFBPEHJeXsKYpjlWtlCuS5iZJqxNZm4RNFzBX5giTX5OqZtMrhne4XcFJfQgZm8vMEpCyhNv67uJ8sSYFcHX7NZyarDYvtVx82nY0NygGaVLsRlAJM2Sqil97C9KIazbWp / rfYKINq9LKivtYKlQhduvDPbdg4vw4Jgvj3NyXyAwTB4kPdN6F6dkZZPJZ7n9ZZGeSe8rNHsfCySdw + NwB9DRtKGIFxlvW0v8P9z7Itl / 9 i0995f7SgSMvfKpQyfXZi2UxSKZ5vHwGf / fEeWzviODs9BLssAJ / gx9 + wqCLRDqoZMZCVhC5GQtre7uwaJ0nx0aRKAUgZQJDMumi2yKJqxS / zxsamU5FBtdI08Gb / WIITw8 / gT + 45 bPIzJ + F6RfRVOqggChHpHuwKjwObPB + ITIWaUK8u6cHp2eHiei23UIAZ5WERamZpqqV1 / xLl2jY / nP7tafO / NVan4 + OKYgYPP4MKqcO4fn0EqL9AfpdDJokUqVyGs8 + cxrDUo6IGq0awbPFzKcpwPz5oSOItDZW1rduTmAFxluiwctHd8OOr21Zs / UzrfHO54sWA9mJOqQLmaDI5qm5NLIUYecJxUpN51GezRDDVAGFUjzF2Nm3nbhYGUkWXM3lUFzIQswXSMPMqjbxHJfp0jICH5ZdjZYlSmvmcrM4NXMEt7Tfhd09N6JUzLHNrZwSX1f7vePZRMkTKX3x3PdhzR + hc6ncZ7Ngq7 + hl08qVdb0z75zN6YIbfJ423DAaM3byQ2ySMdThnyYgqvv6 / OoEN8dotSPMWqM756j4Pup9BzCZLUYAcFoMYZ7M9ZJ8UuwCJoNB6KZsK91Disw3nIBM3O98533PLZz846PrV + 95e uiGkwmyUxVigzwMFgIAz + xTwJFy3kCPTKU52bnkzg / fhQvDz2G46MvQYv4WJcXlMlvzqdMZBnM7wVH3lpk26vWEKo5rrdGmXXMeW30J2gsncMaWafPeI0fZ5q13NehEO0qCfHY8eN4bHAIPpkAFcpbI2oUbcFm3q5JsBRxId1 + b0nCAPA5gQn51MIL78hUUgM + 1 ccj8lgnsVDhAAWTlAmoNnI5izd9Mwi2lOm3BMIyClmdrwhh7oK7FcNpKRUJNi + KopjECoxfyq4rD + x4gKE + 579 IF + WB9HeHvnZ46OBHxqYvvKdcKreIhaLqU0scn2YNQm1Z4amDQWzLuUQG1qLJa7dUv0JAhwQxokIXrKpgHb8p8Lps5j8Fp5kGzzltN / WRBBmz + RkcfvmbiOYUDE4sEbOkcT / Nz2MuAzpc0z + hkYZRcN4gOrVZjWozIW0BQtE2473r7xkwc6VishNnWNHD0xN9O4dTxz4jKkZrMSWTpq9CRUljZmIGXT0 + 5 JPEehlsC0aKIyby0EirG4iImE2W4GsVUaLYIhwKoZRz0rnWePtQZ2dnASswfqnb6jwgcEG / 9 KMDf37ipcHTP744NXx7Ort4Uz6f3VRMFVUlUyIoUnW6wRKoYPk07lZNyh + zaQrXFNL6sA9O4Gyj2rbD02RmuZkvFe1a + atbcFyhQO0p4qxTMwu4SGbXq + 2 q9u5YnhfbbLmMs4yUVYuIFOk205Xa3b8RG8vrCIDJD7V1bn3CwmH / ty / 8510 HJh / 9 k5ge2zlVmBYiFD + spdToidNPUSagIUg +// yFAqLxEJ3HxCJlC5H2CNoDrRiVSMChMtaIGzCRGsf4ZJKlgnbU1ziIFRr / Kvsm7b3xD7N0UZ / 4 x5 / se + HQxROdJID3LKWXbplLTF27lFpstjNFusAsbybzpjndYCUSiE58ryXztmkkSIunGF6J66WmuqaNnC3k2RNxsVGiLdvDEGYNpygAqFZYLjfpXmsCTjmyLrk0caK5k2hItOEbg4No6F5VXL + 4 / x0vzf7wvuHpY / dMzSTW7Qq + Ezsa9qCN8teDB48gQ5N12 / YWTJ9f4jFG31UBJMeTBMlSXBInjrwUQE / rADZ39 + HksWEsFOdQyZsIRWJZihuOYoXGv9rGWIJTqJxxb0MP2w9 / MXg003Vx7PzNZ8ePXzudmLx6KbPUVSyUo2alFBRYDNIcoKBZdaJa0 / Gbgl1jiDjXyxrASMtMtAdNsnjGtp1tY3lvaJcztpxVDPzbeL7YRBUE4RQymf3HzyzgxanvI9nsQ0suf + Nfv / Sfnp2fXwwJusrbEp9IvIrflO + ERXTmhew5bNgQw8XBRSwsVDCwtQVmKo + xkSziPXGIxH2XJ8 / jd3fejb9 / 4 QgOjL6IliYfh1SbIvHhqBo7hRUaV8zOZ / cJvERljG7f2r // iw8bN0Rbz144uHp6abI / lUmtn8 / P3VcU0j1eduQ05nYCJIHVaVo1YNnxp8AlDRY8U + wJzq6tU + ITBsustJcbu + diyp4MB1Ei / x / xSSBrEyZrzPcyZpMjNVXEVCqJxyYeQSQSgRYukXCLlOfa6N / UiDAFWSdeW4RCFGdHtw / jh2ZhpRP4eek7 + PnQFJnsAMo5ky11NYNa9Pg7tl43jxUaV + TWdnfe + e9YnDzObqxK4sTcLv / f7f / iqhNjB3t4WTWlIIrF4EmZsFsVXaFWTFZm3IDL3XLHrKVLTqRs83W9VWLBxa29 / Q5tb + aYzgGcaPCayTDigrTUT3ApKyEwycwuTZUp6q1ApVihgVKeliY / xglDTy5OEfxK2HXAj43b4xD0Ck6 / TOmSJGHLhlaAsL25WcIwumPYn5yHQC6IlRrlc / SbNDXd3bzuwMyZQgYrNN7yNOlyB4tSt7a9Oy / JAuNcuCntj / egwd9EUGaFfF47utR2vujNq8KwvAgby6NjOH7bex6vE77nh4HaY49StKrr051JQd + kgbSxsz2I1kaKttMVjJ9J4eL5POEVPvSTIDdujaAj24HRwzqKkojrb1iHLeJGDB1OoHVVI + INBKhQft / YGoBOwtVNgmhDnac ++ M4PPrNSBXds1MXmlGxYNuuJIfCdy / g + wgTmX0csRNiSiWm64Gqt4PpnL0Vy82D3LieaeFhOj2Ub1ZUPy / hgLBM2vAniGW9XyS3dwtJ0kRfRsdIt1msrFAugNaoSuUGgVCaPU2eSWBOO4F2bb8ZF / znc0XQbHnniKVgRC80xIlMGE5DpYImw6zyBP + wr7N7yroc3brxxEis46kbAdHENyy13nUnNYP / Rn + H65i245ar1 + E5pZlkVhlMRyctzqjgzLiEU4EbXHnlguc + zv7wTrikui6iX + 2 bnHGwdlhSQ0dIZIJ8cpLSsDJ3Am8xCklOaxYqAcBNF6 + 159 Iej2NP5Phw + Mozh / AX0DgQxczbBV3k0t1HAuETZASFn / R3rRm676VceFnizkpUbdSNgSZLLvJURz2spXVIs / GziFYxNHYNOvLFAKZDlUoA88rUsl7S3uWnmUbfh0IVc8Kxiw6xpN6 / ZovMG5SCdo + hlSnx4ZAQPuglU8fvYjVKttIC43omfnn4ebdFOdDT2Q + lZRFOLBoU + fHJoBhem96N1YQN + coow5gYiU85S2qRTXt3mw / rQWkwU55Ep60vb1l7 /+ e1rb17ACo + 6E XBZr + iMAuT1ymwHFEpztGAQ58j3NpMwVc7KOCkO70Tr9mHjaBV3nrVct7q3MFDNl026se4BXcE2jOWHalyxe0yNL7aRTuiYGS2iKRbG9g2d2H31bVgTb8R46ThFxzrmzuQo2CqjXBbxgn8JQ4mXURENFGZpohFh0RBXsSGyEU1aDE / PHClvGXjH97Z1btiPt2DUjYDThYTudLK13ZX7DhDhI0CEQR + sCznXTsBd + un6YtNt3cT + Ojx / dXUD2yaghSDDtDnJX + uPEsRYyjjrxV0Uq4ZROzk3q8Zga4Q7 + 3 xopAOvb7UxU16N7x9 + lLcm1sv0fTQiDJqiaCe82SyUMLmQJ7KBInHCpgN + Ey0WkfptV + FbLz1Kk6Tn8J5rbvvrvXt / bwn4BFZ6XLECZgD + CEZU0gN7EzbpH / 7 yzaZfC7OlIVXzaduez3XVi5thp480K5L3NK6aOjHe1dVklgeXzCK6tS1YUAtIKymsi6 / GU8OPw9 / E1hZXUIvMakEX99kFA0sEWqSIOPinM4 / iOLEfRpA0s7EBbQ0KoXAEjeZLSI9lUCjQJFPoNTLbNhEVVjKOvbfcjqdO / xxC0D + y79 /+ 0 R9uWbflAt6aDg5XtAZLWExvkvRk / 8 nyuHl13zX9yYUJjM3PYWThLFSWo7KjvPYL1UBKcJo5m65g3JDl0notpxiOEfP5ch5bYlshWzN0k5Ar5xCwWb3b6 / Bp1D6nRIIVdAUiQahHyZE3dAURDMj8w / JzGSwSNcgsiOIjYKSJtWwyiVDIwyAo8t2b + pDLzqEx1Gbvufb2xZZIeIMhCyznz + MtGFeygE2L0kvoBqN9Nvf4mld1Boh1UlM4Y9jVtUNe8lpjl + D6YrtKplsCLs1zl6VFE9kJ / ErrZqzufAcOD71KRoDVoDhwKNz3WdVqDuc + 258 iEpN48KaznUgJ4FhMsEZPROmTedZCfoquybwThVTMFmlCkBZTLtwXENBRPoNiwkpfu373j1taOr9dELInho / PJzr31Oo6V3JcsQJ2sepF9 / byh / 5 yd +/ 0 xaEtjOZjRXa24dY3mw7 + zP2tyyx42mq7eDX / j1VmUPpjGk4ebPNOaCLmCgs4N38Wd0SDOJA / 79 ROGbXZYFdRL7g5so0y4c1zxAqx5YpMizXSVFZUqKqEb5NGl4tlpJJst3GBt1CWFIF8u2Fkhfh5Mb715b72Nd + LdO9 + 8 p179rzl7RDrJw + mq5kXbb5g2ydWIYwa7uyEuzWBuFF1FZZ082TLXQDGmsoyQVuihFPTr6I / MYj5hTSRtkJ1pwAI1iVCdgrm6V9RpIDJRyyX6GxdyyqfizqyWabRXj8Q1k3LoAllQtXC081t7Y9t3XjNP63e865De666Lwf8JX4Zo24ErIiqxcpvvFyYC9Zdf2ItQ6 + 4 D7btKplvmV55j1g1t83 + BlRYaZBVgkppy2S5gAdPzxCdGEA0FHEnhpNIeQvU2LBdV6CKxC8RmlYgrxnxhZGs5CETjcloTZXytUq5RCkS7HhDy2J7c + 9 P / cHwP21fc / XLv3v / n6X + WPjqW2KK / 7 lRPwKWJMPLW / lwNapaj7WcQvReJ4kG5ADPgyusNwTBmqxmanPnVXh17KCzrpidj8xoMRbh5TV8DbGLdNlWNSKDt7s4 + 8 P7i1RsdDZ0Ik4sU2ZukC9Ctw2xRMnQXFtL3 / BA /+ qnb9r57h + 87 + YHRhx381Pgg / ilj7oRsChKptu2uQpWcG32AqBlHK7HKtl8b6YKblq7GwenDhI8mEVHrBcx0uB8pYSg2ziVmVSFFRXItb0Qq6bZAw7t2k2UDKIKTYq + Kd5u7CkrfpXiY3FU0QL7V3Wve2H1xo2D0nTr3Pt3M9Lgt / GvOIT68cEi603Dl / mIXqQMwVUwF2cWmI / 1 WCCDryFCppBFqVDEeza8G / vPfwu3r9uO5HSefKVO75OqGn / J / gxs2LVOeNXqDnfyNITb51f39b / W39Y81L1q65m1pjktqdb02i1bRnZ03r0itVQrNepGwDJUa3leCi8d8oIs06qZa5fcZ6 + xBWi / GH4Jn + j6HXxox2 + hR + 3 HQ8d + DLNiuqv4RdeXe + dEtVjAFmuRtO2afAZ1bN2468t /+ fvf / Jzzpn / ElTyueD7YG9WNz0zP73r0HqrIlrfO1ym + s6r5MFvR8Mzxx3FjKIoGYwELqUlUm3XYTtrkjFpVZS3xrUq42pNasMX / aRvDK2XUjw + WZMtdVrYM + IcLcniFdcsAD6t2Y + 2 XxhMXcfyFh3hR + lJm2uljadf8Ldz54wVoy5Gsatgr8I0 / bFb1WS + jjvJgwduRDryyw2ll567t8dgeN0Uyl9VkWdwIoyDL + Ie5aSQXKkgKBJYoQs1fc813fCzPc71JtFzCnOCw + ASQ3mTjkCt11I + J5rsgeUFtraGZ5W2PY9m1GixuynFJ / spNa0MQBt0USaj1kbZrYIiwrIyH / Q2qwSr06R3KYBACSf5 / Aa / 0 kBTye6ILVbk3tpiabQfA9g32Fn1X2wBfkr86OS1lQVCX7evgmXqv4M52AzQWfLEtCPrjfTCYTa8GdyyatlhLxbrYcYWNuhEwXVddpHCXt1RwI2fWlKw92oZN7VchqkZ5kxPL6 / ds11Y7WG7K4 / lt0d0Cr7boTKhtqcOKcCl77I53I + qPUYCmw20X7xwvSJYoiivSZvCXMepGwIZFebAoehA0 / 0 chQbA9H7Z1bUGAyHunatItuHPLddiq + oDiduHhgnfMu + gWDdQiZM8X21gTGUBvYy + KlRyvHBFcK8AMO8GldsXQ6yaKrh8TLfLAxq615XfACYM1NtVTyFUKfC9Ehi2zTSmZyFgzk554F3GvMd7A1FxWK + 1 UdtRW7bPB0uFSpYw10V6sb16HxcwS3w3Grn6g4 / 3 ZTmyok1E3AhYNy7Z0Z2PiKolPf3P5HJ4 / 8 hhBj2n + RHdDO7b1bCPivoCAFsB1 / duxmHW2bPcCL09zncZoHu7sCJhNgvncDHqkBQhGprovhPMWzkQJtinbqJNRNwKWlfhIyN96hNfSLIMT80THncyVYcJJdcYTk9jcsRYbu7egM9qBBmJ7EukkReFCrcuZq43VvYhRO58mKTi3cApnTz0Oo5TltVxemiQKcqUp3HMk4I + NoE5G3Qh4V / OmIxv7r / 3 vxaI5VyoXq9ExC5YUlQEPzvayTHMPDL6Iuze + F +/ axDbTWKJI2FjW2a5G / 6 H2VDWXZtvnLdHx3xyZw3iuwCNv5r9LRAGWSubc7qtv / Yv3XrPzIOpk1I2A2XolVfQfX9u1 + dHGSMtB2xKzXud3VIvcLUqDZJyaOo + 54 SdxfbCIsYmjfEdvwYU5vc2wPAjSXYIGwcuF2Oko0S1pqmu6TQq0lFx7vOfQpp6djwd8kWNr1txZF9vasVFHSBbw + Y9 + afTE3FN / eOTk6VWPPvPtPz4 / NX4XpU6GaZiq7WLFTFA + nx + vjBxCbvoUhucyfAcWPuxqoFQzzba7dNR7jk15t4BPlJSybtjq2u7Vz /+ b2z71f9x5ww0jn / vcl68otuh / NupKwG6dVv7Qoa + cOd698W9j0Z6DTaFoYnL2zPsnF0Z254umVCyVmcbhQiiEs / M5ZA2Bl85Y7mJxvv8zr5M2yKQrTk013YqUbhGJb9uijz1rru3d + sLG / q0 / IA65MR6LH73zhr2n6fPrJj3yBm8gIni7RtXp + OYTX9rxnSe +/ vlwJL6wa / Puw0fPHNkyPvvi / SQ3NZeTE5oUWzKRbopF7HhJN5AtyEsBf3xRENPxgN9ozKTNytqeXQ9tXb / z5JFzL2wrFlPN99 / 6 +// 1 fbd9qG587ZsNNl3fFgI + dOiQMpE91yko / sTeG / dmT4y92PDlb /+ f / y2VS7UNdG55dKBn7enXjr90zezSiQcYi9jWtOUru7becHBifnjThakT9 / jk2Nxv7P0P / 3 XXxl1Lr776k0hGMBpv33k3W + VXdxr7usEF7O1w8LYaT778ZEOmPKM2o29xz549BmtUduqVA5tE0xI23nDzqTtW31F57rnnpALGmjQzqt96694l1Of4l5ZECP8PAPgFhqae3ywAAAAASUVORK5CYII = '), ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 0, ' test', ' {"春江潮水连海平":"海上明月共潮生" }', ' m', ' m, -xl', 2, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (4, 0, 32767, 1, 65535, 8388607, 1, 2147483647, 3428724653, 1, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, ' 2021 - 01 - 01 ', ' 2021 - 01 - 01 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2022 - 08 - 09 T10:17:16.161342 Z', ' 00:00:00 ', ' 00:00:00 ', b' 1000001 ', ' 70 ', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 0xfffd, 'тест', 'Airbyte', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -0 xfffd, -'тест', -'Airbyte', -'Airbyte', -127, -'Airbyte', -'Airbyte', -'Airbyte', -'Airbyte', -'Airbyte', -FROM_BASE64('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'), -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -0, -'test', -'{"春江潮水连海平": "海上明月共潮生"}', -'m', -'m,xl', -3, -127, -0, -0 - ); - -INSERT - INTO - test.TEST_DATASET - VALUES( - 5, - 0, - 32767, - 1, - 65535, - 8388607, - 1, - 2147483647, - 3428724653, - 1, - 9223372036854775807, - 10.5, - 0, - 10.5, - 0.188, - 1700000.01, - '2021-01-01', - '2021-01-01', - '2013-09-06T10:10:02', - '2013-09-06T10:10:02', - '2022-08-09T10:17:16.161342Z', - '00:00:00', - '00:00:00', - b'1000001', - '80', - '!"#$%&\'()*+, - -./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 0xfffd, ' тест', ' Airbyte', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 0xfffd, 'тест', 'Airbyte', 'Airbyte', 127, 'Airbyte', 'Airbyte', 'Airbyte', 'Airbyte', 'Airbyte', FROM_BASE64('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'), 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 0, 'test', '{" 春江潮水连海平": " 海上明月共潮生"}', 'm', 'm,xl', 3, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (6, 0, 32767, 1, 65535, 8388607, 1, 2147483647, 3428724653, 1, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, '2021-01-01', '2021-01-01', '2013-09-06T10:10:02', '2013-09-06T10:10:02', '2022-08-09T10:17:16.161342Z', '00:00:00', '00:00:00', b'1000001', '99', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -0 xfffd, -'тест', -'Airbyte', -'!"#$%&\'()*+, --./:; - -<=>? \@ [ \ ] ^_\ ` { | } ~ ', 0xfffd, ' тест', ' Airbyte', ' Airbyte', 127, ' Airbyte', ' Airbyte', ' Airbyte', ' Airbyte', ' Airbyte', FROM_BASE64(' iVBORw0KGgoAAAANSUhEUgAAAHgAAAB4CAYAAAA5ZDbSAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAEIPSURBVHgB7b0JnFxXeSf6v3vtVV29791q7bIkS8iy5U3IC2DHNnIAJ2MYQl4AZ4HJQB6TmcybQQz5ZfHkDXmQkEcCgQDBwWb1IrDBu4wX7Vu31GpJve / Vta93e985996qlu1k3vzUJiq / d + xS13LrVtX9zrf9 / 9 / 5 jmDbtgTAQl2Mzwn83zc8vfzBPn777Gc / az / y8qd9iaAULvoSoQYEVdGW1FLFtCuo6AFR0EoFcWkhX1xoX6uK5YnSr5fE4v7PXPPI3Oc + B + ENH / 1 Z79z / a + Oz / 8 KjX8IQBBKwQMPG22Q8e / Hrvuni + e6Z / MjWRD6xOVcsblRDaJMEKeRXA2pZL6NoFso + ySc3CO1TF3MjL / cG + 62 oHJCazYYH77v + C0W8TQbJVqxbAbvfm9 / 1 nvv2kf / cO5ub + r2hpeO3SIF8d3bJipiirQX8klAsmYhFJdJdoFQxIKoG + qUd9lx5utTh7zRHkxdHFb / wj / F4 / JsP3vizabwNRl0L + OsX9 / mkhH63rAVeKKbThaHcqVv8duDP5tPT61PSFIKKhsSChfYOGZm0iWIZdF9EuWwjlbIQiZsIlltwXWQt5spFfH / weciajcbG8NnmePw / xiK + px + 88 aUs6ngwAYuo0 / GRvn1l0YRhl / MfP7z48mdGps9 + KSI1rNfJw8ZCKpKLJjRVhKaJKOVtiORWRUGERDejIsCqkDbLSxg ++ QouXDyE5rYgZNOH5FJh3cTs9JfG5xY +/ fs / v7YVdT7qVsBkne2AobxUKVU + nEjPf6ot3NbdqDXBktIwSFuNiohgQIRtAqYuQBIFLmSZ5rRgC6gUBWhBCSdVHRdME9GwhEhMhmgpqJTQNbew9AcLudQff / LcJzXU77DrVsD / 49 RvxZ9KPfFHo3Pj / bd0vyd0 + 8 CdmEmPQdJ0FAqOIMn3ks8VINB9SWIazAQtck02dHqORB5t89F7NHosoKlJhk + TIQsyFFMNT4zNfzRx / Mm /+ E8 / v7URdTrqUsD7Satmpi7 + 2 sTs5Icmkxel7koRPUYOaWMMgqTCIuExASuyCL3CfqTIBUui5pos0V / bEkCKS5OAjiWB6yWaFAppcViGYJL2B2Vogg9zi6n7p3ILv / bFc3fUpSbLqMPx2tjYuuml2Y9WimK8pCxg7MgzSPiDKHUWYCsKLNMm4Qpca5kQBf4fuGlmkbdIQrbovmGQgDUJqmLRRLDofQLCIYkHYYJoIxwRkMyUGtLZ1EeHRuQDdIoTqLNRdxr88MMfkJbKyX +/ mExu7wy3CLYUwvdm0vju2DiskEzaS8IzBKgq99P0GJegFlzQgiNsg46TScs1P2k03SffyzVaIW1nViBA51MFhVKs4rZ8qfzpD9gfkFBno + 4E PNPZuGEhO / 1e kBk1pQLWxq + CNtCI8NY4FEWGadiUHgASaTBlgKSpcCXK3i04Nxv8GJM0XaL /+ iK9dJzlmHOJRd5M4CIiShQ + OqdgyEgVU / dseq6wAXU26krAtr1PHE8Nvz + XL8RVUUPJKqKk69i16h0IR8kcWxKa1Q4IFvlV2RUqEzBJ2WaStmuoiM3vCCgZJTSHmtAR6kWhrPO4U1UF + IQgOoJ9kFUToqnSxKk0jGem3s8sCOpo1JWA /+ TFc + unc6N7YSiUz5IQpABOz59El7oKm1p2kHBU9Ac3U0RsgWX4lOg7grRdbSaNtZiwXZttsRSK / O5w + hRu7L4FQTFKQIhF + bOAgdha0mgy1z5HyyWTgJP8wr0nmzLrUUejbgS8b98 + cWppYpuulzuhUypDAZRBwtINHacWjmKTdguujl8Pnx1BhaBIFmBxSXKttbmwmW9m0bMjdGcCEAGBVDmBUjmHa9puQJne61f9GGhch / nkPFTZxyNvwST / blodk + mFbey7oE5G3XzR7l8vBHPl9LWkdTEWGbNImEfIZI5Hc8MYnziNO / x90IqTBG5QFOz6WNs1ylybBfD3sKcsemzQcRbxaJrow7GZw9gavwZhqREdsQ60BtuRyCTJUkg83bINiVIOMVYs5q7133YkiDoZdZMmnZsYaa6US9vMsihblN4ITMAWS38oy6WI + fjCY4icUTFMM8Ai6TJBsuNIhnRjYha4MJnkTQqoVMpxo4qfhJyFT1YxsjiM + cQc3tX + a7A1C + n5DArFHILwQaUMuJRh75FlA8bVk5lcC52pLnDqutHgXCbZXjb1XstyzC4LjG2XxVboQTHmw / esLA6WS8uDZRK0ibZQuxNvuT7ZMA2sb7qK3qdApwnBoA9m6l8ZewUdUhu2aHHMpYd5rsysgMzUgO6LFICZhtGXz6XbUSejbgRs2 + Y6EkKzaYluLuv6UvoFpMykxZS79kYgNwYpRRId6dKtTMltR6iHTG4XSLdJmw00BVqxunE9JrMTXLgEZJGflXBudhCzg99A6eLjkDPDzEzwScRSLvYZLFc2TLPJFsy6CbTqRsBls7LVNHSfTZmMIpJKSY6PZZrMI2YBDgxJwZWkOpPAoNdFst8XE6PY0bKH + 1 pm1vf03Yn5zAwFVhUedLGZwHLonF3AM + dO4NDRQxim1yVJ4lrMcWz6DF1nGm35KqaxBXUy6kPAJEfdNPqYUrK8tyfWSxBjkAdKXLKuNnMTDF6nwn8Ze45FyfPZWQSFCK5qvh5doTVYE9mIwZlBsFya5cfMk8s0YRSfgsG4D8 + 1 KFiKyPziML8tkAYrCglXdxLpimH0A29S1nMFjroQ8Bd / 8 km1WMr1MhDCpCuuyQG0 + LsoWrb5ZWYC5RbZFbJ36W0X2SgSRHVubhC7YjtxT +/ dWMokMM00lCyB5fpzhmApqgR / RIHcqkH1Oabb8fekxbITgTONLxvlno88 + 5 G6IB / qQ4NDoUjBKLUw2SmChmRhAX2BDRQQy9w3Mg00bU / ADqEgeCgWDYmkc3HmWRhDj6Bp9hiOjnyPgiudR + B2VYAi97UsOmePWZ7tBHJOxCYrDkHB + OWyXm5pj + bDqINRF2mSikozYUlRJpKyUUHOSCKuNhEs2YnpwggJxVcNqrjGcWBDqGoz86Epo4zXBk + iOTyMEQq0JGaTBds9hgRMAhQMoYpuitIy00 // kTvmxzKSgiLpSChXaabDFnCFj7oQ8FIl0RhVor5NTVuRD5tYKJ9HhXDoTS3bcHFkiI7wuWiVIwzBkQ3XalYQzIj + ok / GqxoFU5KBBEGQErFF / D2We7zo5lZM + 1 lULdUidcudKJ6ACSDxpXKZZtTBqAsBL + Tn4xO5cbFIZMA1TTfhzoE70YgZ9GpLeIHSJoNBjlYtL7axrIZQcLRSIh8rxxT6xaStCzoHQNiRpu0d5ppkOKZekpY9ZjZBqPl327LEpXKmAXUw6kLAAdUfYFUYS6UkHh16FBdmT2EHZbVILRBAYTooFWpWmmue4DJGEr0qOVEX11HRI / 3 dZ5gph1DVevYUO5fInbtHPzkns90PsemOYhNIXQejLoIsyxRjzExKFnGzRAnO2GN4sjSJR5ZyKBHMJLjpkWXVTK5DIjkskjMurQx2BOpMBCZiLtDqrOAYB2pxms35YnZjE8Qg9Ktimqu + cujjCq7wURcaXCBzKAquFgqsWkOBSTpdIdpQEfQqW2QvW4DD7jv + 2 PGp3l + XPXQKAfhxbu4MR6jV2eHhnQDHrttCHUgpCRSsRbRF2jHQMBCbyQ5f8fXk9aDBQkUwNEdKIv / GAg + AGU8LBl9hWeL7etlUzS7XVNHRbp5Y0R0H1nYnjlQ9hYedVKtAdIq6G / wxbGjehLAvirvXvx8lwyjue + dzJq7wseICvmhf9Nk1u3jZ42H7YdHUjbBnUr2aKsffuigF4Aq3pn38K7D6ZyIRdNtwyu74oRbPZdc3beC + lP3HymqdhFpAdY2Hd0dwrMBsbg6bWrfhroH7UdEtHJl9LbLvuXde8dUdKy7gzNx4 + 9 DMgb2zs7MrwpkOPvKIVDFLmuBKlftVuKAGP6L2L / fBtlDVPpHUcnXDGvRFVpGiqyBRM7IAmWIWzYFObG / bRdBnCc7U8KJm582iW8fF4zRCvOZz82jLDOKGiB8 / Hf4R + 3 zf6YX5K94CrvgX1FuDk5VysUUXFt9DmqwNzTzbh8sY8VCbQEGN5GivUI2VLGuZOYaHYqEW + dLxBmnuTHYGcV8jbu29Czd23IMmfw8v1TkzPYRb +/ YiojVz + FN4U5MuVIMxdufw0PN44vi3sFCcpbRMltpClRWzVG / VWPEga4ewQ3 / 21 LeeVqF8NFkeb64oha0 / vLDv2YpVqJALtSq6oKtiMKWa2tixdcXZfcK + f3Ft8pI / KxAEfUm0aguUpkgqYmoIGXuharMFr0RHsFxEi3Bos4ij0wcxlj6DXe034v6BTxAmPYXB6aPoJvp + V + MAThunavCml / l6YbQ7p1RNxpOk7VpmETalXoZtKll / L706git5rHwUTdfm7x / 6 Qk + j1LznzvZd1xRDF4TX8qd + m / kt5uaIiaHgyG / 4 NP98KBv74dMXvvZoj9n +/ Jo1d5bf7HQRLU9MEs0JTvoyzJnyXpJ4WI0gGG7C4tIkCSPgfHS12Aq1AmgeDkhIG2k8v / Q9nFh8Adc27sVdLZthj / 8 Y15dnMEHRVp4Fa2xiCF7k7dw80ISHckEJop + AEp2lSoayOJy94k30SgtY + OA3tl87m57 + qwVpdoM2fR5NQRVGj48D + xXD5pFwNluRm + PrOnQ787GF0Gg4mt / 67 D93wsxiUTBtQ2SXuSXYDMUoUgS7QLRfFLIVgkcecUiRf4NloIbgEg / 0 bpmwx4ZwjFKuLL554u / xC6IP90Z0zJEACx0qLllnadeAjyoAIgq10mqR5eamhM4rfxnxigr4o9 + 5 dvP56bE / t0VjQ0tzBBm / QsSAjLCkcD5VrlgMtkdFLWGV0AJVGVAPzO9 / r92p / 4 je / sM3Oye7hJZg8fA2S8K5rucmjFpH4FPCEMqaK1yhFk3Yy6BKHmQT + S + 6 kTIdFgz64fNZGM5l8ZWszc1tv + ScR / QyLvcvnxySd39Znizwgvor3v + ysSIm5vGxLzf8 + dO / tXNycfqPBVG / qa2hCbGoD6YgIxLT3HSGKAEf4cYkq4bGIE6ffhzHzvwY87Pp6NHFn3 /+ W2f + 4 zVvll7dtmqHqEk + mb2SLmfw3MhzuLX5HtxMOSmMPBfgcuzZ9vJX70zM5FYF5kCQql + FP6DBChH3G9Uga5JTICDWrghHubx0a1ng5f0WBn5XUtYVL + QVEfAaeQNRqdrnTal4y0DLgNAajfGVBSZf + 0 NAASH6rLJRJFOpE8rvJ + 51 oknBfMSEavqQSWdXn0sd / rUfn30w9Ppzx + 3 OoCr5Gi2KdFmpDsOjXzz9AwgXfoEyYdIQnfjLsm039629t / awJgfmV2Ui9hW26IwYJo1ubFVhNUpnuDbDmimt6gp3UWJlOni1wIrvZLRH2vgicmsFc / 23 cqyIgGPBrmhMiV + 3 rm1tcFvbtcjbCU7GM7JcIBPIAizdsHiNlE73TRJyIKJBIsiRr8DPUXBaTL77TPrkqtef26eEpJgck1kNM3sfu / AT1jweunAaJ1IZXlEpkm31SwGaPAZ / TxWd4jehWoXJgQxWX8VuLsHPiH5Pwz1wg93ViXdeHVuPgBTk582Xc9i9 + ha0R3sIONEdjqIOxmUL + OFT + 9 S / eeazv7060Bv5t2s + hemlObqA7AI59S5suUhFN2GQgCvkg8t04xE1aTOLqH1 + CeW8gEwhvcGolO72znt89sngoUOHlFCwRWj2tQpMg5kHZ2AGNAWzTUGUaZLwGilTR2ewGz0RuvhW2Y2mXR63KgbBBTOc6JgJWZBc4VYPcScGTZiSVUKpZGB7 + 7 XIlNK4pnM3dnTdhKHFk65LsKDGxLc / Fn1samjt0Mih22IzRzAxehhnFk8TvRfgzU4c8IFMoB3ktF6ZaTIJSndXHlR0MtsszKPwt1wypYyRvpmgSYn5YlnU1kpti / dKsBtjvmZLslWHzXENoyo6VZReffTw0llsbd / GiQCDBC5QdNSgNjoZk0v91aJq1Eh + Nx1iEVY1kILz3ERiFFuar8XG6A24a9XH8NS5HyNFLkKURNRHiHWZAmZrdC6On7tmdGGh96vP / ACPHH8IBsrcRDJhssGsZrPWwbWPpUmsMpGZW6YFTJN5XM0WalNumcovbtYuGk2s6082XThfrKTXpSrT / 6 VN8XVq0HgO7Gmkg0O4 + DH9jJJexrnFs7htzR0kMwqixCBWRda7KbEbJS0TilCNlr1zOPcdxpBZGBnT6Sn00ON / t + 2 jWMzO4eDkS + QiFO8LWJjBFT8uS8DlG481LCYTewqmFTvZJmIpJIIZLbZyvlJxsMNsoYSo1EyBCUXTlDKxSoqKZXO6jvtkw + mAYxkSMuVMy3z6NF + De92aOzNlQf3S4viB2eTx73XLyTxdfKlamuMhTB6eoZJQT06fRClfxK9f9RF0 + Tbwmi2LJkWNRbKrsKPnbZ2Hzpn4Kgb6T2PltDSDysUF6GPfgaK / ip9d + BvKCpw2EDbnj2H6G6S3t4men5lrKJX11UwB / ORLmallmsXwDNNwNJhepwsloD + 0 BT3 + DfCCJdv1p0zIHKFik0KviFk90eKdf0 // vSndCP7t4UThUIJsg1QjjuBppHeFOfdLQd2z559GuxjBPZ07IOpJdyWhXaX + ariowLngZl8zBXIatzosmOr291OUH + ATwwj58Mj5YXz70MO4kBtjq / 2 rkbloi3XR / vGyBJwspZpK5UoHC1iclXu2uyjMFRwYNElmlczb3ubdmEkvIEAX30eRKTuWvcbWBrHByAP2WDClSzraDLZHz463 + gd1f43BYwIT3FjZkZ3TA4uZ1bnSIl45 / U00TByAMf8K / 4 lWtV5LqL6HSYkR + UE5hu7gGpTMAiFjGjFPa5EsLnFcWw1IOOgT8Hw5D4NpL / fj3moKQc / N + q54IV9ekGVgTblstPAVALazes9rfGLx5ic2D3ZGp4 / hyRcexFh2BG2 + XrT6u1CgC9oYaEV / cC2Z7IoDNVqsSL1wSb3x6cGHmboY7Ly1paBwIuplnC0DMFgts0a56mulBfz1mefxQi7JAzGvEseDGkWPVKAn54jn7fddw9cO9wZX0 / t9yJl5nnqxq6MQOKNqzn37Eo8u6KE29Yo30ZcFVZoVq4fSH02hC8CnsuWu27W9khmBR5yjGqVBUoHkRB7ODCCqBdCkdWF9fBfOFA6wJkduyY1NOLN + yYqBhlV + O380b1lV0NkhFRzuvloS6UbDIq / ysIj5Od / sp1SHXkpZ1fdU0yfHk3PAIl1KQM8b2BrZjcZoB1GBY5DJEjh + flm1CHAJQiba / x / IgyuG3kQCFllkyYANy3KCGNaOyAuA2D9aQIE / EmAr5MkEG1gX2kw + 8 jdwnqLeheI8xwi9VQkk5Uu + U0c2bEuiaGGZBrp0L5ZXc / DKSfchy2J8hFZJYg1mdKBGp85ZJexZoaCMN2IRTJxPnsD7V / 8 bbO7agvE0 + VpZdX + LXYviPKDE + 3 xJNBuS / rd5HixIDQzAYELtDnRWQQS + 4 Nqu5TPsOXbRGbLVlruAzYQpH55 + BUfmjkAyFUfjPc0Q5Ev82saFZpo6km55BtqqMUVerToP3VlttItSCV5OC6fo3XJ1lrdVog / bFL + Kt2ZgX1HTNFw0BoHCGCLCBRQqC9yiwJtwQg0NWz4kQdKTDave3j6YctqoSBchXy4gphHB4I86F5JrpF2rsoDjB9lykSOJaTx0 + Bs4nHoRiqw5DnGZdjIjvvwzBgc32Yqilt2X + bJR55y2m7 + 6 HtEFLoRlWkukPIJKCKsa + ilC1zmypQpBtAS6mK / nFKIsyTBUA8eP / l8YI / LDZOtTq6sU3S / huoZqWY / NQZbypsFH3uYabEPjESxd1FyhiN74KjSq7WjUWuni6vAyVWeJCAUsBD9Nh2UcDDpBEfOBPAiyHY0EX4urXkL8E5hi2QZK / Bq7FB4b1eJ270CpptHeMWR / sZCb50tcOiKrkNUz6PVTIEVCNuxKFfvQiGx4Qs / iH8ankePrRWvOVvQQL3cqVX86hQv79l35nfIvS8CO4RXgI02cWJjA1qZr8O6 + XydSvcBfXw4F83iIhOALEMpEPDE3oyJcSg7uMn1W86zpb / gcRTCWrQp1nvOiYaH2bTiR4LoDjx1ine8OT7yK96 +/ n9dj7Wy7hcxwmtdhcTfCIE / 6 FmY8iArlvZIkumnYcqRMcBml2vel8xuog3FZAib9sxwxSxSx5hDPXUSvlECimKYrITlNT + BWOdn2JasJ + PAqF5flt6IgvaHWWJFEY3nQ5pzLNcvLNM07DReO + 8 tYADieGsNschQf6f8QOtUGzOVnl388PMrXYZk8Hy46 / lxwKyy563E + j88LqT4EfFlpkiJJJQ / mkyg1OXD85xhJqDCDPt7eiKU9kofzehdHWJZNVoXNLqQTPcmi + gYNVmzV4GbeM71VYt4Ngqp0n / u6Vz / tCpmBoQdHH8ceeSd8kWGkc8MsmKue37aWR + ZOqscE2 + 7 vwLQ + RS9W6LfK2NS + DhfypzlWLdnS21 / AkqSkuBmkCySTf32mXEIzUX8NsQAYFM04YElwyH6 + ms + NUBhcKTBOEXZ1lZ9H5SiC / EYNVuU8 / zyBdbdja5Ec0yC6rBC3EIKDEnsm23YlJikMHxcwhxweO / szIvkpDjAplJOdz / fMh2OURRelEnknnpARxobmzTiTeQ13rPkATl64iFKlxIv + FE3JoQ7GZZloikqzXFAkJUach0IKAqTJ3nWzDMcJx8gs8ooLi1UjMtLe7 + LKQhXBcpqGMi194zYDqqwU2PX3qRo6Ix1VYr9qol0 / KQrVKKuqzXz1PvlVmybGiRYNh + N + 6 PSdRXt5tYfgfgHvSZE3QJvJzGBrw424vv1OdKjrcWz6NW7y3eLLMupgXJaANVFNy0TVsN5VDLFiQrYMZ40lJx1IwBbRg / 3 RAQL0A7yFUaPWRnjvAAladxZTk3SDxB / LrskUX2dVvvjKJyOKrDQTRQG / 5 qcUpxmGbjglrZ4QmeKKtY47ooSqv3fWMjmdcjSfwrvpiLWlD9V1v3wRWjUkt3nbh6XSElrp + 927 ai9eHX0O8WCELWV1UDBRvCwT / fFDH1f + 9 MX73 / I1xpcl4EAgNCXLss6YI8FyfKRecYMR1jafNNswKgiJjWgL9iGuteCWgdtxLjnCu8uxC5st5rEq3ofuSA + XSskoX7LkJU7UQ2e4s7M13IqwFkHYF6kFbEBt9QFvmGVfgnR5QTYTMOuU46VPHt3oAVWOFaHvzzy9FxlaTqB18dg / 4 MzxL2CqMIT1TVsIvavwDvL5YrGCyxjtxoRfV + fv2HforgDewnGZZIM0LctKngcprPuNopB2ma6AWaWrwp / P59PY2rgL9679MMaS57CYT / GotzdCxAORD4ocIs3s4ILJVZbiy5t9fmjNlzKJYurAzo6dhF2vc0ykx + dWAQhUm7Gw + zLRet7aIsezwrXCQjWy915womKbs1veC + z7M8TL79ewv5TFI3ND2N51I4qVMipmhft0glf / lwTMtgT48on7GzwYXZAlSsXFVQHLirDH9ltUhnt5ebCMeb + ipkl / USkaaA41oyvah0Ipj4iPEKTYKueCpAdxY4MKozKPw7OH6GiCJ4luSpbmCZfegj1tN6CLtJNpzVJxseeuj911yer5dClXmkyOYysF2EpmhFdSem6WV0fZNUDFJ / vRH + 5 zGSo3WXbKw5xo2eWHqyYctedd5 + Iea3EQxmzUEGzqx1VNu3Bx6RyPvtl5NVX5f2WiWX / pBwdv2p7XjX9vSuX / 9 tdn3 / e // 83 Q +/ 8 g5rd / x6 / 5 rrXN4OYvvHzvhgeP3vOWtEe8rCi6s6VvcXJ2cjaTy / QZJR1LwhLuXv2reHbph9jafDXlnlMctpwmjvbxn34JrxKGIShB7t + YNpXtIn545oeIZU8hY + dJ7D7M56c2LxaG ++ n0p73PiQYj5umJI / jZ7CimkgaUtiDXUObnw0oELcEo5o0E96e95O81csKsvEdwF / 06E KNHNbo9tZwHVZPOA0IWaLF2w0zgpu0K2sa1zbchkU5gIT + HYEhx0j / 5 jYDM68cXT + 4 dOG6c + 8 RUYu5 / W9vUH6kQsjaTP0eZgIgcxREd4gCCdvQaUxT2R0T8F7wF47I0uD3WkVckdY61BzQqFvnTFOYWF8gUfxAD4QECGKZoBklIxTR8O6MjzcthHFSoQn6bbYgR8wfx8 / I4XqX3SoaMvJFrPz738tVs1y7vc3xSwDDp0SHypRMBCsjcWirmI1nX9y7ilJuJYy4bBgZi65AsLBGvqy4DVxxtFUWhSht63QAEYZnQeTRv8TSJlX + xGCKstGFTbDuOTb3Cg0bTaVlLiLn4Ly7 +/ pOhvY0L1sSfBYSmB5r90UhTZSPidAuKFGwW / PCZYaiZJFpnkq0XRk / cniol34u3YFyWgDsVf17zaaN019CLzg8 / M / oy3tPQQb6yiAwFUOUCCdIvI9QcYkutyT9aXEsqZOBYKqoS4C8oPviCGvQy74MRH0 + P3PniyCPVyo6QGK5Q3mqpdB7GPQNV94uyWSK8eQLv6bmXC7qdiIQ0Rb + M0 / Uap9ieM4YLny6r8HBycdb6HTz9alKbEZGilMMb5HNLuL5xHfzkbudLM9jRvQsbm65CSA3aiiz / sz74i / vv0LL5id / J6vP3qKWo // bGj6E / tJ20tZEmtEYgiYIGvw8JmixDxjmYcrZrMTWy7 / mJr + 7E Co / LEvB7byjmGyKNxwi2y7B11CqZxoSVxo9 + 8 iAOj30PPjJnuZwJq2wgEJSJwWFVFxbHfvn2CGQ22f5ERZocskLvJ9DfzEOaz0 // yk9G9 +/ 1 PicUjZdIq8zlvK5BEvLTxGhUGwldGiYTOoIHb / oIhMoc8lahinPzH7ksomajBm3Y5BY0xKVWvhBcJhexrfE6pAlqLVF6F03quEddQJs4gd +++ pMUL6zGVGKSRf5mQPWV3uya7LP3ifPx3N0Vcel3hVxYnSMq8vzIEWylQBOLh8HairMKUkIBUSSTra2KIhwKUwRvNL948Vt /+ vSFb / eyc6xUl4TLC7KEfdaq1u4LPk1K89ortmohJONvzs / hdCKPcEQhTaDUJ68TSMFyUY0CsArlkiwtEZEtEVrkIy0z6ceWTDREibojnqJUKEVHFk7 + hwdf + Mx29jmqJfMtNRhcyKJoizX9JgF3RztJm0Jc4EfG9yN74XuYHP0pPTZrDJUXTXtSXkbwsbsG5earQ5s5jdhLAmSNwtN6GgJd / DS5lVcPDuK1l36Cbz77f + O7xx4ma7HI0iRLfRPEjY3gL843Fc3sr8qG0mJVFErtAjiROoz9I /+ I00vD3M3rvPjf5pgBWwyg030fxSbT87NXjyV + cfc7R / q3fuHlT69Im6bLLnzvaF97MRaKTjPzVskZBCbQzw / 4 SKgmfBpr7SuhkDdgl8vY2L4JIbmbfmCWm2bNbuBUfJwmRb5AP1i0SMhkqgmYzBbSq185 /+ QX / uDxD7wjFozlKLCxGLq0rXsrN6UM0OhvHECeLcqmc82T8P786Kt4ZmHWhSHZqIZT7hCqgZMTd4sos51bjBxu6bob13behInUKFkimaNz5aAfX01l8c3EEI6nTzslRywFo3ua + uYCzliJfsr9r9OLvNslxwVkYqlm + 31 Yiiq8TJhBuIWSw2alycKx51S6FrpuRVOl + Y8HhcjtXZO7LivP9sZlCzg02j29rmfts6ago0yCFEi7fGEVubRJwQSBHA0 + 0 k7S2HwFF9NnsLFlBzY33g6RfOe2tp0olYLw + YmY1xQsLNIEobS / Ma7R8SKSmfTNJ0ZPPPzcyecfkE2lnC5mkCql8L6rf53y5m60B7uRKae5uVOCKiYb2HIWv9tn0vl + QvVfwWlPCCda9gAOFtEfnTuMW9vWYWtLBGdmT / E2wwy8UVjH2a4wLII3FQra1jb2c19NCJyhioE3FYCuV24ulko9hbzlnN8MkhFRuNYyQeZLNl ++ ky1YPC1LZQyQasDKBSlVvFoKon1tQA5P33fffSvSweeyBcy + yIaujc8HAj6jVKHZmClDDRLgQd8 + NUf5MJldgTXbzpr0Ywt48eJTiFNk + qtrfxMbmjagOdiGxWwBDSRYH5nf + cUyIU86WlpV3vSskC + sOjp68BOlrBFULA2nZwYp2hbxgdXvIdNdIQ2sQHDXovgUZ79Crw1wbRWEI3AGYdrLYGcPFCkYRQy99iWcO / 4 Q68nFD3KreTlmzjQ + 6 mtAM6FpLPgC + 2 Z + 35 ti0cVycaNhWJKui2Bb3cblTr5tbZldG93RXjZ52GM22YpMKUgMFxKj2Brbgk69VxwcO9WJFRorsrqwqaH / YFtj6wVW41xYqvDoOBgIQ8 + SOTN0vpIwVyRTxdYiUUT63ePfgZzNYHe8jFVKCamCQCS8jqaYSZFwL / x6D9KFPDra / IgEGEBSUYl + lC1D5MtSXjqzH82Jw6hMPUdmXawiWE4xnFBtV1htyGLV + GPwVRW1kkgme5ny0q8Pnsc3Tp5iWzJxgVqmA3swJqxBimNb + zuc1Rj0GyVBNMPWG2lNtnFXvlzq0 + mV / tB6skpRyGR67YpaXYBn8r2bTLJcvK6YMgebFx0uUcaRzD + P2cH9yvn5Y +/ ACo0VEfB9Ox5Idzf1PqQEZCOT01Eiv9XR0IrVLRswM5pANEK + y6cimdJBd + Enc / r1I3 + Prz / 5 V5hauICA7KMfZxMVR5PAN49NrRtwa9dvIJcidiooYMtAH2l4ADqBKXq2gtlSGn839CoeHSdMW2FrhWyOQ1dX4PPhthS + 5 FfW9kyCWzrrdIUXkGmJIhsLccLEEbBzY / sohewY9vS8kyxNgiNczETbfv8bBPyZp877C + V8O3vP8NwwtvvejR2hjVBIiKWSY6KLrGN9eBVNWgktvh4YJYEvxpNJ4w8sjOGhc4cwkRzdfGjq0Ipg1CsiYDY0NfTTxlDjxb72AVTSpFV0cVa1rMLSUgF2sYwIQX4lSg / KBR0NGmlGKIYfkf8ZpZncqlm8D + USWcd0sYjTC08SL5zA7133R1gXvAXv6n0fOprjaGqmqDSo8H0XJsN + JEMB + CWVbn63rKbK / AOvWwwOd1WClwPbbnWmkyuzrj3OYvVqC2HLbedP51kqJJHPTlHckOMlwYTOGZKovQGqXNM44NdNPSLYEjKFHF4Y / jGGDn6fXNUkp0otvjzHoHMY2NJwAyJWF1m2LCy + WRcRM + RDpKYQoVyp5n5fWxwrMFZMwNubNp5RFOXpdL5ovGvLXSTEVrRSGkMOGOn5HPyUGqkUTabJFwtl8k0hiqL9AWQrjGIkHDtI2svy6DRpE5nzQ3NPYDr1Ah64 / ldxe / 8 WpGaKSKUqlDcLWNfWwzeNNAW2SHuABOyDZS2rf6vSSB575Arcc8wOn1t7aNeowhq75LZiosg9ayQxPfhdaIU0vcabdhhhFii8bmjIRwnRCzFBUiKFpJ3G94lcmWGd9ixnvRZbBjuWGkNzhlK55Az5aZsv1mPfSSczrpB / K5XzkenUaBtWYKyYgH / z3n2pmzZf / 43 ZzMLZV44exG39m7AmCjKtDUiSX85PphBtUCD5fJhbMiAVC2j0U6KvEf6csciEVYiooFw4oCCbFTG / pOKxocfx3Cv / HefPfI3AjgKlXWFMzZegp6NEKGzggc + q6GqKrDM80KoSEJYnKMuBFj1 + kGmRbbsL36rFHG4xgnOzvD7SrO2EYfOemMxnfu21szg4lySolZlwUT9y + uwbKipLphjSbVNx9sG0WdUHAs0ESYZUvlaLCZLt48SqRb577Ac4ljgOTaC00HSWxQbIX7PvWCjryoW5U01YgbFiAmZjx469x66 / ets / naBI9 + kDj2DsxENQNdJYVUNiQYe1ROBHTCUySEV6SUKTEEEDaa5GsF0yJ2BhiXw0kTSdjRL8NJMzOQ0Pj47iT08cgkmutrtJwKaBCLIYRa +/ H3vX7UWCzGeRomCxVoDlprsEHoga3 / zKdLV7uRK7pFL1L5aR / Q7Z4EwQm3cWINAjTumO6hTEU6hn + kOK / frfnytkw7ZpqiLXcieaZ5OHJ0QE7LDAjQufFTo0RiCQObZIa9lnsdrgJqWdofW88wGheSuy6ceKCvjONXeWb9103d + u6u0 + 8e TYRfyPA + dgMf8bU3jJzPxEGnoqjViTRpbbj7DcizwhXi0Btts2oVymjJlEBcVsiVISG90tCuWiGtIk3bklYGZRRyqtE7iRhZkbxC1hC + fnjvNVhU6ZkNviwXLua2S6r2reSuR8obp9ju0eV6vkpP8Nh / Bng88Bbk5tvrxFI / SNoWYa24vJ9c8EuJhEILxBgzOFjJ9t1RSUw2jzd / DWEhVL53XiIQ5kmHxZKovMne4EjpWw + cpKQvjEGE3KAAWblBuXzBWRzYoKmI0Pv3vf / KZ1Wz4VbGg8m5Nke3EyDztXgj9GWulrgr / YBoOE6AtI6GrsI59Mgp / MIlgpoCXCut8EyOTKmJgnQeeLaA4Y6CFTH / OzHUdVCtRkErKI16aG8JdPfhVzxQThyc5yh + VlrSw4ms3MI + iLYHPzduTyeSeq5qbS2aGy1sHdrpIQzrC5D95EWYAM1dnk0vZcOueYrXz5jeuSSkIlyNIoduy2zu18j6du4sSvjr0DE5kJXl4cFuOEw5OlUmMICTESusEnJE + hKO + Pa7303h2IBRqxEmPFBczG2sZdL28c2PgFSVXnGdyTmi1ALBIB76sQsN6IXb3vQn9gNWlpA1pbGwloIGHMlVGczyIqltEUloh39ZPp9WM + JRCiVaEou0QXp0zaYKAlSuZbVXFMDXBzD5cCNGsSc3wyCfTViwewu / dWshZNvCaMmW62l6FsK + 6 m0U465G0gzXL5iBil + KCNr9RIE40puBOGB10202zNiDQF3yDgkBKJ2PQZRb1In9eIq5tvxjWR3Tg4fJDn5uxzmtCLuK8P6xrWIpvLV9fasX2MUUni3vU7ETJaMJedvTKQrDcbn77v08VNa7Z85 / qrr / 8 LuqRm0WS7eRZhE3gxNH0Eh4aO4ta + jVgdTKNVKBOcqXH8mlw0FmgyFBPEHJeXsKYpjlWtlCuS5iZJqxNZm4RNFzBX5giTX5OqZtMrhne4XcFJfQgZm8vMEpCyhNv67uJ8sSYFcHX7NZyarDYvtVx82nY0NygGaVLsRlAJM2Sqil97C9KIazbWp / rfYKINq9LKivtYKlQhduvDPbdg4vw4Jgvj3NyXyAwTB4kPdN6F6dkZZPJZ7n9ZZGeSe8rNHsfCySdw + NwB9DRtKGIFxlvW0v8P9z7Itl / 9 i0995f7SgSMvfKpQyfXZi2UxSKZ5vHwGf / fEeWzviODs9BLssAJ / gx9 + wqCLRDqoZMZCVhC5GQtre7uwaJ0nx0aRKAUgZQJDMumi2yKJqxS / zxsamU5FBtdI08Gb / WIITw8 / gT + 45 bPIzJ + F6RfRVOqggChHpHuwKjwObPB + ITIWaUK8u6cHp2eHiei23UIAZ5WERamZpqqV1 / xLl2jY / nP7tafO / NVan4 + OKYgYPP4MKqcO4fn0EqL9AfpdDJokUqVyGs8 + cxrDUo6IGq0awbPFzKcpwPz5oSOItDZW1rduTmAFxluiwctHd8OOr21Zs / UzrfHO54sWA9mJOqQLmaDI5qm5NLIUYecJxUpN51GezRDDVAGFUjzF2Nm3nbhYGUkWXM3lUFzIQswXSMPMqjbxHJfp0jICH5ZdjZYlSmvmcrM4NXMEt7Tfhd09N6JUzLHNrZwSX1f7vePZRMkTKX3x3PdhzR + hc6ncZ7Ngq7 + hl08qVdb0z75zN6YIbfJ423DAaM3byQ2ySMdThnyYgqvv6 / OoEN8dotSPMWqM756j4Pup9BzCZLUYAcFoMYZ7M9ZJ8UuwCJoNB6KZsK91Disw3nIBM3O98533PLZz846PrV + 95e uiGkwmyUxVigzwMFgIAz + xTwJFy3kCPTKU52bnkzg / fhQvDz2G46MvQYv4WJcXlMlvzqdMZBnM7wVH3lpk26vWEKo5rrdGmXXMeW30J2gsncMaWafPeI0fZ5q13NehEO0qCfHY8eN4bHAIPpkAFcpbI2oUbcFm3q5JsBRxId1 + b0nCAPA5gQn51MIL78hUUgM + 1 ccj8lgnsVDhAAWTlAmoNnI5izd9Mwi2lOm3BMIyClmdrwhh7oK7FcNpKRUJNi + KopjECoxfyq4rD + x4gKE + 579 IF + WB9HeHvnZ46OBHxqYvvKdcKreIhaLqU0scn2YNQm1Z4amDQWzLuUQG1qLJa7dUv0JAhwQxokIXrKpgHb8p8Lps5j8Fp5kGzzltN / WRBBmz + RkcfvmbiOYUDE4sEbOkcT / Nz2MuAzpc0z + hkYZRcN4gOrVZjWozIW0BQtE2473r7xkwc6VishNnWNHD0xN9O4dTxz4jKkZrMSWTpq9CRUljZmIGXT0 + 5 JPEehlsC0aKIyby0EirG4iImE2W4GsVUaLYIhwKoZRz0rnWePtQZ2dnASswfqnb6jwgcEG / 9 KMDf37ipcHTP744NXx7Ort4Uz6f3VRMFVUlUyIoUnW6wRKoYPk07lZNyh + zaQrXFNL6sA9O4Gyj2rbD02RmuZkvFe1a + atbcFyhQO0p4qxTMwu4SGbXq + 2 q9u5YnhfbbLmMs4yUVYuIFOk205Xa3b8RG8vrCIDJD7V1bn3CwmH / ty / 8510 HJh / 9 k5ge2zlVmBYiFD + spdToidNPUSagIUg +// yFAqLxEJ3HxCJlC5H2CNoDrRiVSMChMtaIGzCRGsf4ZJKlgnbU1ziIFRr / Kvsm7b3xD7N0UZ / 4 x5 / se + HQxROdJID3LKWXbplLTF27lFpstjNFusAsbybzpjndYCUSiE58ryXztmkkSIunGF6J66WmuqaNnC3k2RNxsVGiLdvDEGYNpygAqFZYLjfpXmsCTjmyLrk0caK5k2hItOEbg4No6F5VXL + 4 / x0vzf7wvuHpY / dMzSTW7Qq + Ezsa9qCN8teDB48gQ5N12 / YWTJ9f4jFG31UBJMeTBMlSXBInjrwUQE / rADZ39 + HksWEsFOdQyZsIRWJZihuOYoXGv9rGWIJTqJxxb0MP2w9 / MXg003Vx7PzNZ8ePXzudmLx6KbPUVSyUo2alFBRYDNIcoKBZdaJa0 / Gbgl1jiDjXyxrASMtMtAdNsnjGtp1tY3lvaJcztpxVDPzbeL7YRBUE4RQymf3HzyzgxanvI9nsQ0suf + Nfv / Sfnp2fXwwJusrbEp9IvIrflO + ERXTmhew5bNgQw8XBRSwsVDCwtQVmKo + xkSziPXGIxH2XJ8 / jd3fejb9 / 4 QgOjL6IliYfh1SbIvHhqBo7hRUaV8zOZ / cJvERljG7f2r // iw8bN0Rbz144uHp6abI / lUmtn8 / P3VcU0j1eduQ05nYCJIHVaVo1YNnxp8AlDRY8U + wJzq6tU + ITBsustJcbu + diyp4MB1Ei / x / xSSBrEyZrzPcyZpMjNVXEVCqJxyYeQSQSgRYukXCLlOfa6N / UiDAFWSdeW4RCFGdHtw / jh2ZhpRP4eek7 + PnQFJnsAMo5ky11NYNa9Pg7tl43jxUaV + TWdnfe + e9YnDzObqxK4sTcLv / f7f / iqhNjB3t4WTWlIIrF4EmZsFsVXaFWTFZm3IDL3XLHrKVLTqRs83W9VWLBxa29 / Q5tb + aYzgGcaPCayTDigrTUT3ApKyEwycwuTZUp6q1ApVihgVKeliY / xglDTy5OEfxK2HXAj43b4xD0Ck6 / TOmSJGHLhlaAsL25WcIwumPYn5yHQC6IlRrlc / SbNDXd3bzuwMyZQgYrNN7yNOlyB4tSt7a9Oy / JAuNcuCntj / egwd9EUGaFfF47utR2vujNq8KwvAgby6NjOH7bex6vE77nh4HaY49StKrr051JQd + kgbSxsz2I1kaKttMVjJ9J4eL5POEVPvSTIDdujaAj24HRwzqKkojrb1iHLeJGDB1OoHVVI + INBKhQft / YGoBOwtVNgmhDnac ++ M4PPrNSBXds1MXmlGxYNuuJIfCdy / g + wgTmX0csRNiSiWm64Gqt4PpnL0Vy82D3LieaeFhOj2Ub1ZUPy / hgLBM2vAniGW9XyS3dwtJ0kRfRsdIt1msrFAugNaoSuUGgVCaPU2eSWBOO4F2bb8ZF / znc0XQbHnniKVgRC80xIlMGE5DpYImw6zyBP + wr7N7yroc3brxxEis46kbAdHENyy13nUnNYP / Rn + H65i245ar1 + E5pZlkVhlMRyctzqjgzLiEU4EbXHnlguc + zv7wTrikui6iX + 2 bnHGwdlhSQ0dIZIJ8cpLSsDJ3Am8xCklOaxYqAcBNF6 + 159 Iej2NP5Phw + Mozh / AX0DgQxczbBV3k0t1HAuETZASFn / R3rRm676VceFnizkpUbdSNgSZLLvJURz2spXVIs / GziFYxNHYNOvLFAKZDlUoA88rUsl7S3uWnmUbfh0IVc8Kxiw6xpN6 / ZovMG5SCdo + hlSnx4ZAQPuglU8fvYjVKttIC43omfnn4ebdFOdDT2Q + lZRFOLBoU + fHJoBhem96N1YQN + coow5gYiU85S2qRTXt3mw / rQWkwU55Ep60vb1l7 /+ e1rb17ACo + 6E XBZr + iMAuT1ymwHFEpztGAQ58j3NpMwVc7KOCkO70Tr9mHjaBV3nrVct7q3MFDNl026se4BXcE2jOWHalyxe0yNL7aRTuiYGS2iKRbG9g2d2H31bVgTb8R46ThFxzrmzuQo2CqjXBbxgn8JQ4mXURENFGZpohFh0RBXsSGyEU1aDE / PHClvGXjH97Z1btiPt2DUjYDThYTudLK13ZX7DhDhI0CEQR + sCznXTsBd + un6YtNt3cT + Ojx / dXUD2yaghSDDtDnJX + uPEsRYyjjrxV0Uq4ZROzk3q8Zga4Q7 + 3 xopAOvb7UxU16N7x9 + lLcm1sv0fTQiDJqiaCe82SyUMLmQJ7KBInHCpgN + Ey0WkfptV + FbLz1Kk6Tn8J5rbvvrvXt / bwn4BFZ6XLECZgD + CEZU0gN7EzbpH / 7 yzaZfC7OlIVXzaduez3XVi5thp480K5L3NK6aOjHe1dVklgeXzCK6tS1YUAtIKymsi6 / GU8OPw9 / E1hZXUIvMakEX99kFA0sEWqSIOPinM4 / iOLEfRpA0s7EBbQ0KoXAEjeZLSI9lUCjQJFPoNTLbNhEVVjKOvbfcjqdO / xxC0D + y79 /+ 0 R9uWbflAt6aDg5XtAZLWExvkvRk / 8 nyuHl13zX9yYUJjM3PYWThLFSWo7KjvPYL1UBKcJo5m65g3JDl0notpxiOEfP5ch5bYlshWzN0k5Ar5xCwWb3b6 / Bp1D6nRIIVdAUiQahHyZE3dAURDMj8w / JzGSwSNcgsiOIjYKSJtWwyiVDIwyAo8t2b + pDLzqEx1Gbvufb2xZZIeIMhCyznz + MtGFeygE2L0kvoBqN9Nvf4mld1Boh1UlM4Y9jVtUNe8lpjl + D6YrtKplsCLs1zl6VFE9kJ / ErrZqzufAcOD71KRoDVoDhwKNz3WdVqDuc + 258 iEpN48KaznUgJ4FhMsEZPROmTedZCfoquybwThVTMFmlCkBZTLtwXENBRPoNiwkpfu373j1taOr9dELInho / PJzr31Oo6V3JcsQJ2sepF9 / byh / 5 yd +/ 0 xaEtjOZjRXa24dY3mw7 + zP2tyyx42mq7eDX / j1VmUPpjGk4ebPNOaCLmCgs4N38Wd0SDOJA / 79 ROGbXZYFdRL7g5so0y4c1zxAqx5YpMizXSVFZUqKqEb5NGl4tlpJJst3GBt1CWFIF8u2Fkhfh5Mb715b72Nd + LdO9 + 8 p179rzl7RDrJw + mq5kXbb5g2ydWIYwa7uyEuzWBuFF1FZZ082TLXQDGmsoyQVuihFPTr6I / MYj5hTSRtkJ1pwAI1iVCdgrm6V9RpIDJRyyX6GxdyyqfizqyWabRXj8Q1k3LoAllQtXC081t7Y9t3XjNP63e865De666Lwf8JX4Zo24ErIiqxcpvvFyYC9Zdf2ItQ6 + 4 D7btKplvmV55j1g1t83 + BlRYaZBVgkppy2S5gAdPzxCdGEA0FHEnhpNIeQvU2LBdV6CKxC8RmlYgrxnxhZGs5CETjcloTZXytUq5RCkS7HhDy2J7c + 9 P / cHwP21fc / XLv3v / n6X + WPjqW2KK / 7 lRPwKWJMPLW / lwNapaj7WcQvReJ4kG5ADPgyusNwTBmqxmanPnVXh17KCzrpidj8xoMRbh5TV8DbGLdNlWNSKDt7s4 + 8 P7i1RsdDZ0Ik4sU2ZukC9Ctw2xRMnQXFtL3 / BA /+ qnb9r57h + 87 + YHRhx381Pgg / ilj7oRsChKptu2uQpWcG32AqBlHK7HKtl8b6YKblq7GwenDhI8mEVHrBcx0uB8pYSg2ziVmVSFFRXItb0Qq6bZAw7t2k2UDKIKTYq + Kd5u7CkrfpXiY3FU0QL7V3Wve2H1xo2D0nTr3Pt3M9Lgt / GvOIT68cEi603Dl / mIXqQMwVUwF2cWmI / 1 WCCDryFCppBFqVDEeza8G / vPfwu3r9uO5HSefKVO75OqGn / J / gxs2LVOeNXqDnfyNITb51f39b / W39Y81L1q65m1pjktqdb02i1bRnZ03r0itVQrNepGwDJUa3leCi8d8oIs06qZa5fcZ6 + xBWi / GH4Jn + j6HXxox2 + hR + 3 HQ8d + DLNiuqv4RdeXe + dEtVjAFmuRtO2afAZ1bN2468t /+ fvf / Jzzpn / ElTyueD7YG9WNz0zP73r0HqrIlrfO1ym + s6r5MFvR8Mzxx3FjKIoGYwELqUlUm3XYTtrkjFpVZS3xrUq42pNasMX / aRvDK2XUjw + WZMtdVrYM + IcLcniFdcsAD6t2Y + 2 XxhMXcfyFh3hR + lJm2uljadf8Ldz54wVoy5Gsatgr8I0 / bFb1WS + jjvJgwduRDryyw2ll567t8dgeN0Uyl9VkWdwIoyDL + Ie5aSQXKkgKBJYoQs1fc813fCzPc71JtFzCnOCw + ASQ3mTjkCt11I + J5rsgeUFtraGZ5W2PY9m1GixuynFJ / spNa0MQBt0USaj1kbZrYIiwrIyH / Q2qwSr06R3KYBACSf5 / Aa / 0 kBTye6ILVbk3tpiabQfA9g32Fn1X2wBfkr86OS1lQVCX7evgmXqv4M52AzQWfLEtCPrjfTCYTa8GdyyatlhLxbrYcYWNuhEwXVddpHCXt1RwI2fWlKw92oZN7VchqkZ5kxPL6 / ds11Y7WG7K4 / lt0d0Cr7boTKhtqcOKcCl77I53I + qPUYCmw20X7xwvSJYoiivSZvCXMepGwIZFebAoehA0 / 0 chQbA9H7Z1bUGAyHunatItuHPLddiq + oDiduHhgnfMu + gWDdQiZM8X21gTGUBvYy + KlRyvHBFcK8AMO8GldsXQ6yaKrh8TLfLAxq615XfACYM1NtVTyFUKfC9Ehi2zTSmZyFgzk554F3GvMd7A1FxWK + 1 UdtRW7bPB0uFSpYw10V6sb16HxcwS3w3Grn6g4 / 3 ZTmyok1E3AhYNy7Z0Z2PiKolPf3P5HJ4 / 8 hhBj2n + RHdDO7b1bCPivoCAFsB1 / duxmHW2bPcCL09zncZoHu7sCJhNgvncDHqkBQhGprovhPMWzkQJtinbqJNRNwKWlfhIyN96hNfSLIMT80THncyVYcJJdcYTk9jcsRYbu7egM9qBBmJ7EukkReFCrcuZq43VvYhRO58mKTi3cApnTz0Oo5TltVxemiQKcqUp3HMk4I + NoE5G3Qh4V / OmIxv7r / 3 vxaI5VyoXq9ExC5YUlQEPzvayTHMPDL6Iuze + F +/ axDbTWKJI2FjW2a5G / 6 H2VDWXZtvnLdHx3xyZw3iuwCNv5r9LRAGWSubc7qtv / Yv3XrPzIOpk1I2A2XolVfQfX9u1 + dHGSMtB2xKzXud3VIvcLUqDZJyaOo + 54 SdxfbCIsYmjfEdvwYU5vc2wPAjSXYIGwcuF2Oko0S1pqmu6TQq0lFx7vOfQpp6djwd8kWNr1txZF9vasVFHSBbw + Y9 + afTE3FN / eOTk6VWPPvPtPz4 / NX4XpU6GaZiq7WLFTFA + nx + vjBxCbvoUhucyfAcWPuxqoFQzzba7dNR7jk15t4BPlJSybtjq2u7Vz /+ b2z71f9x5ww0jn / vcl68otuh / NupKwG6dVv7Qoa + cOd698W9j0Z6DTaFoYnL2zPsnF0Z254umVCyVmcbhQiiEs / M5ZA2Bl85Y7mJxvv8zr5M2yKQrTk013YqUbhGJb9uijz1rru3d + sLG / q0 / IA65MR6LH73zhr2n6fPrJj3yBm8gIni7RtXp + OYTX9rxnSe +/ vlwJL6wa / Puw0fPHNkyPvvi / SQ3NZeTE5oUWzKRbopF7HhJN5AtyEsBf3xRENPxgN9ozKTNytqeXQ9tXb / z5JFzL2wrFlPN99 / 6 +// 1 fbd9qG587ZsNNl3fFgI + dOiQMpE91yko / sTeG / dmT4y92PDlb /+ f / y2VS7UNdG55dKBn7enXjr90zezSiQcYi9jWtOUru7becHBifnjThakT9 / jk2Nxv7P0P / 3 XXxl1Lr776k0hGMBpv33k3W + VXdxr7usEF7O1w8LYaT778ZEOmPKM2o29xz549BmtUduqVA5tE0xI23nDzqTtW31F57rnnpALGmjQzqt96694l1Of4l5ZECP8PAPgFhqae3ywAAAAASUVORK5CYII = '), ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', ' enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', 0, ' test', ' {"春江潮水连海平":"海上明月共潮生" }', ' m', ' m, -xl', 3, 127, 0, 0); -INSERT INTO test.TEST_DATASET VALUES (7, 0, 32767, 1, 65535, 8388607, 1, 2147483647, 3428724653, 1, 9223372036854775807, 10.5, 0, 10.5, 0.188, 1700000.01, ' 2021 - 01 - 01 ', ' 2021 - 01 - 01 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2013 - 09 - 06 T10:10:02 ', ' 2022 - 08 - 09 T10:17:16.161342 Z', ' 00:00:00 ', ' 00:00:00 ', b' 1000001 ', ' 99 ', ' ! "#$%&\'()*+,-./:;<=>?\@[\]^_\`{|}~', 0xfffd, 'тест', 'Airbyte', '!" #$ %& \'()*+,-./:;<=>?\@[\]^_\`{|}~', -0 xfffd, -'тест', -'Airbyte', -'Airbyte', -127, -'Airbyte', -'Airbyte', -'Airbyte', -'Airbyte', -'Airbyte', -FROM_BASE64('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'), -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -'enQPSYCmDJBWtMJlV3kHBq4m2OQaTf5SbOH6eSGUqotmtAwWzw', -0, -'test', -'{"春江潮水连海平": "海上明月共潮生"}', -'m', -'m,xl', -3, -127, -0, -0 - ); diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/hook.py b/airbyte-integrations/connectors/source-mysql/integration_tests/seed/hook.py deleted file mode 100755 index 488bcf605f2e..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/hook.py +++ /dev/null @@ -1,209 +0,0 @@ -# Copyright (c) 2024 Airbyte, Inc., all rights reserved. - -import datetime -import json -import random -import string -import sys -from contextlib import contextmanager -from datetime import timedelta -from pathlib import Path -from typing import List, Tuple - -import mysql.connector -import pytz -from mysql.connector import Error - -support_file_path_prefix = "/connector/integration_tests" -catalog_write_file = support_file_path_prefix + "/temp/configured_catalog_copy.json" -catalog_source_file = support_file_path_prefix + "/configured_catalog_template.json" -catalog_incremental_write_file = support_file_path_prefix + "/temp/incremental_configured_catalog_copy.json" -catalog_incremental_source_file = support_file_path_prefix + "/incremental_configured_catalog_template.json" -abnormal_state_write_file = support_file_path_prefix + "/temp/abnormal_state_copy.json" -abnormal_state_file = support_file_path_prefix + "/abnormal_state_template.json" - -secret_config_file = '/connector/secrets/cat-config.json' -secret_active_config_file = support_file_path_prefix + '/temp/config_active.json' -secret_config_cdc_file = '/connector/secrets/cat-config-cdc.json' -secret_active_config_cdc_file = support_file_path_prefix + '/temp/config_cdc_active.json' - -la_timezone = pytz.timezone('America/Los_Angeles') - -@contextmanager -def connect_to_db(): - with open(secret_config_file) as f: - secret = json.load(f) - conn = None - try: - conn = mysql.connector.connect( - database=None, - user=secret["username"], - password=secret["password"], - host=secret["host"], - port=secret["port"] - ) - print("Connected to the database successfully") - yield conn - except Error as error: - print(f"Error connecting to the database: {error}") - if conn: - conn.rollback() - sys.exit(1) - finally: - if conn: - conn.close() - print("Database connection closed") - -def insert_records(conn, schema_name: str, table_name: str, records: List[Tuple[str, str]]) -> None: - insert_query = f"INSERT INTO {schema_name}.{table_name} (id, name) VALUES (%s, %s) ON DUPLICATE KEY UPDATE id=id" - try: - with conn.cursor() as cursor: - for record in records: - cursor.execute(insert_query, record) - conn.commit() - print("Records inserted successfully") - except Error as error: - print(f"Error inserting records: {error}") - conn.rollback() - -def create_schema(conn, schema_name: str) -> None: - create_schema_query = f"CREATE DATABASE IF NOT EXISTS {schema_name}" - try: - with conn.cursor() as cursor: - cursor.execute(create_schema_query) - conn.commit() - print(f"Database '{schema_name}' created successfully") - except Error as error: - print(f"Error creating database: {error}") - conn.rollback() - -def write_supporting_file(schema_name: str) -> None: - print(f"writing schema name to files: {schema_name}") - Path(support_file_path_prefix + "/temp").mkdir(parents=False, exist_ok=True) - - with open(catalog_write_file, "w") as file: - with open(catalog_source_file, 'r') as source_file: - file.write(source_file.read() % schema_name) - with open(catalog_incremental_write_file, "w") as file: - with open(catalog_incremental_source_file, 'r') as source_file: - file.write(source_file.read() % schema_name) - with open(abnormal_state_write_file, "w") as file: - with open(abnormal_state_file, 'r') as source_file: - file.write(source_file.read() % (schema_name, schema_name)) - - with open(secret_config_file) as base_config: - secret = json.load(base_config) - secret["database"] = schema_name - with open(secret_active_config_file, 'w') as f: - json.dump(secret, f) - - with open(secret_config_cdc_file) as base_config: - secret = json.load(base_config) - secret["database"] = schema_name - with open(secret_active_config_cdc_file, 'w') as f: - json.dump(secret, f) - -def create_table(conn, schema_name: str, table_name: str) -> None: - create_table_query = f""" - CREATE TABLE IF NOT EXISTS {schema_name}.{table_name} ( - id VARCHAR(100) PRIMARY KEY, - name VARCHAR(255) NOT NULL - ) - """ - try: - with conn.cursor() as cursor: - cursor.execute(create_table_query) - conn.commit() - print(f"Table '{schema_name}.{table_name}' created successfully") - except Error as error: - print(f"Error creating table: {error}") - conn.rollback() - -def generate_schema_date_with_suffix() -> str: - current_date = datetime.datetime.now(la_timezone).strftime("%Y%m%d") - suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=8)) - return f"{current_date}_{suffix}" - -def prepare() -> None: - schema_name = generate_schema_date_with_suffix() - print(f"schema_name: {schema_name}") - with open("./generated_schema.txt", "w") as f: - f.write(schema_name) - -def cdc_insert(): - schema_name = load_schema_name_from_catalog() - new_records = [ - ('4', 'four'), - ('5', 'five') - ] - table_name = 'id_and_name_cat' - with connect_to_db() as conn: - insert_records(conn, schema_name, table_name, new_records) - -def setup(): - schema_name = load_schema_name_from_catalog() - write_supporting_file(schema_name) - table_name = "id_and_name_cat" - records = [ - ('1', 'one'), - ('2', 'two'), - ('3', 'three') - ] - with connect_to_db() as conn: - create_schema(conn, schema_name) - create_table(conn, schema_name, table_name) - insert_records(conn, schema_name, table_name, records) - -def load_schema_name_from_catalog(): - with open("./generated_schema.txt", "r") as f: - return f.read() - -def delete_schemas_with_prefix(conn, date_prefix): - query = f""" - SELECT schema_name - FROM information_schema.schemata - WHERE schema_name LIKE '{date_prefix}%'; - """ - try: - with conn.cursor() as cursor: - cursor.execute(query) - schemas = cursor.fetchall() - for schema in schemas: - drop_query = f"DROP DATABASE IF EXISTS {schema[0]};" - cursor.execute(drop_query) - print(f"Database {schema[0]} has been dropped.") - conn.commit() - except Error as error: - print(f"An error occurred in deleting schema: {e}") - sys.exit(1) - -def teardown() -> None: - today = datetime.datetime.now(la_timezone) - yesterday = today - timedelta(days=1) - formatted_yesterday = yesterday.strftime('%Y%m%d') - with connect_to_db() as conn: - delete_schemas_with_prefix(conn, formatted_yesterday) - -def final_teardown() -> None: - schema_name = load_schema_name_from_catalog() - print(f"delete database {schema_name}") - with connect_to_db() as conn: - delete_schemas_with_prefix(conn, schema_name) - -if __name__ == "__main__": - command = sys.argv[1] - if command == "setup": - setup() - elif command == "setup_cdc": - setup() - elif command == "teardown": - teardown() - elif command == "final_teardown": - final_teardown() - elif command == "prepare": - prepare() - elif command == "insert": - cdc_insert() - else: - print(f"Unrecognized command {command}.") - exit(1) diff --git a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/requirements.txt b/airbyte-integrations/connectors/source-mysql/integration_tests/seed/requirements.txt deleted file mode 100644 index 97b176cab978..000000000000 --- a/airbyte-integrations/connectors/source-mysql/integration_tests/seed/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mysql-connector-python -pytz \ No newline at end of file diff --git a/airbyte-integrations/connectors/source-mysql/metadata.yaml b/airbyte-integrations/connectors/source-mysql/metadata.yaml index 496beba3e8d5..4d69bdc8fdd8 100644 --- a/airbyte-integrations/connectors/source-mysql/metadata.yaml +++ b/airbyte-integrations/connectors/source-mysql/metadata.yaml @@ -1,7 +1,7 @@ data: ab_internal: - ql: 300 - sl: 300 + ql: 200 + sl: 0 allowedHosts: hosts: - ${host} @@ -9,10 +9,10 @@ data: connectorSubtype: database connectorType: source definitionId: 435bb9a5-7887-4809-aa58-28c27df0d7ad - dockerImageTag: 3.7.3 + dockerImageTag: 3.9.0-rc.1 dockerRepository: airbyte/source-mysql documentationUrl: https://docs.airbyte.com/integrations/sources/mysql - githubIssueLabel: source-mysql + githubIssueLabel: source-mysql-v2 icon: mysql.svg license: ELv2 maxSecondsBetweenMessages: 7200 @@ -22,54 +22,11 @@ data: enabled: true oss: enabled: true - releaseStage: generally_available - releases: - breakingChanges: - 3.0.0: - message: Add default cursor for cdc - upgradeDeadline: "2023-08-17" + releaseStage: alpha supportLevel: certified tags: - language:java connectorTestSuitesOptions: - - suite: unitTests - - suite: integrationTests - testSecrets: - - name: SECRET_SOURCE-MYSQL_SSH-KEY-REPL__CREDS - fileName: ssh-key-repl-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE-MYSQL_SSH-KEY__CREDS - fileName: ssh-key-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE-MYSQL_SSH-PWD-REPL__CREDS - fileName: ssh-pwd-repl-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE-MYSQL_SSH-PWD__CREDS - fileName: ssh-pwd-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE_MYSQL_PERFORMANCE_TEST_CREDS - fileName: performance-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE_MYSQL_ACCEPTANCE_TEST_CREDS - fileName: cat-config.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - - name: SECRET_SOURCE_MYSQL_ACCEPTANCE_TEST_CDC_CREDS - fileName: cat-config-cdc.json - secretStore: - type: GSM - alias: airbyte-connector-testing-secret-store - suite: acceptanceTests testSecrets: - name: SECRET_SOURCE-MYSQL_SSH-KEY-REPL__CREDS @@ -107,4 +64,11 @@ data: secretStore: type: GSM alias: airbyte-connector-testing-secret-store + releases: + breakingChanges: + 3.0.0: + message: Add default cursor for cdc + upgradeDeadline: "2023-08-17" + rolloutConfiguration: + enableProgressiveRollout: true metadataSpecVersion: "1.0" diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlQueryUtils.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlQueryUtils.java deleted file mode 100644 index adf8a108a921..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlQueryUtils.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static io.airbyte.cdk.integrations.source.relationaldb.RelationalDbQueryUtils.getFullyQualifiedTableNameWithQuoting; -import static io.airbyte.cdk.integrations.source.relationaldb.RelationalDbQueryUtils.getIdentifierWithQuoting; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.source.relationaldb.CursorInfo; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManager; -import io.airbyte.integrations.source.mysql.internal.models.CursorBasedStatus; -import io.airbyte.integrations.source.mysql.internal.models.InternalModels.StateType; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import java.sql.SQLException; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlQueryUtils { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlQueryUtils.class); - - public record TableSizeInfo(Long tableSize, Long avgRowLength) {} - - public static final String TABLE_ESTIMATE_QUERY = """ - SELECT - (data_length + index_length) as %s, - AVG_ROW_LENGTH as %s - FROM - information_schema.tables - WHERE - table_schema = '%s' AND table_name = '%s'; - """; - - public static final String MAX_PK_VALUE_QUERY = - """ - SELECT MAX(%s) as %s FROM %s; - """; - - public static final String SHOW_TABLE_QUERY = - """ - SHOW TABLE STATUS; - """; - public static final String MAX_CURSOR_VALUE_QUERY = - """ - SELECT %s FROM %s WHERE %s = (SELECT MAX(%s) FROM %s); - """; - - public static final String MAX_PK_COL = "max_pk"; - public static final String TABLE_SIZE_BYTES_COL = "TotalSizeBytes"; - public static final String AVG_ROW_LENGTH = "AVG_ROW_LENGTH"; - - // Returns a set of all storage engines used by the configured tables - public static Set getStorageEngines(final JdbcDatabase database, final Set streamNames) { - try { - // Construct the query. - final List jsonNodes = database.bufferedResultSetQuery(conn -> conn.createStatement().executeQuery(SHOW_TABLE_QUERY), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - final Set storageEngines = new HashSet<>(); - if (jsonNodes != null) { - jsonNodes.stream().forEach(jsonNode -> { - final String tableName = jsonNode.get("Name").asText(); - final String storageEngine = jsonNode.get("Engine").asText(); - if (streamNames.contains(tableName)) { - storageEngines.add(storageEngine); - } - }); - } - return storageEngines; - } catch (final Exception e) { - LOGGER.info("Storage engines could not be determined"); - return Collections.emptySet(); - } - } - - public static String getMaxPkValueForStream(final JdbcDatabase database, - final ConfiguredAirbyteStream stream, - final String pkFieldName, - final String quoteString) { - final String name = stream.getStream().getName(); - final String namespace = stream.getStream().getNamespace(); - final String fullTableName = - getFullyQualifiedTableNameWithQuoting(namespace, name, quoteString); - final String maxPkQuery = String.format(MAX_PK_VALUE_QUERY, - getIdentifierWithQuoting(pkFieldName, quoteString), - MAX_PK_COL, - fullTableName); - LOGGER.info("Querying for max pk value: {}", maxPkQuery); - try { - final List jsonNodes = database.bufferedResultSetQuery(conn -> conn.prepareStatement(maxPkQuery).executeQuery(), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - Preconditions.checkState(jsonNodes.size() == 1); - if (jsonNodes.get(0).get(MAX_PK_COL) == null) { - LOGGER.info("Max PK is null for table {} - this could indicate an empty table", fullTableName); - return null; - } - return jsonNodes.get(0).get(MAX_PK_COL).asText(); - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - public static Map getTableSizeInfoForStreams(final JdbcDatabase database, - final List streams, - final String quoteString) { - final Map tableSizeInfoMap = new HashMap<>(); - streams.forEach(stream -> { - try { - final String name = stream.getStream().getName(); - final String namespace = stream.getStream().getNamespace(); - final String fullTableName = - getFullyQualifiedTableNameWithQuoting(name, namespace, quoteString); - final List tableEstimateResult = getTableEstimate(database, namespace, name); - - if (tableEstimateResult != null - && tableEstimateResult.size() == 1 - && tableEstimateResult.get(0).get(TABLE_SIZE_BYTES_COL) != null - && tableEstimateResult.get(0).get(AVG_ROW_LENGTH) != null) { - final long tableEstimateBytes = tableEstimateResult.get(0).get(TABLE_SIZE_BYTES_COL).asLong(); - final long avgTableRowSizeBytes = tableEstimateResult.get(0).get(AVG_ROW_LENGTH).asLong(); - LOGGER.info("Stream {} size estimate is {}, average row size estimate is {}", fullTableName, tableEstimateBytes, avgTableRowSizeBytes); - final TableSizeInfo tableSizeInfo = new TableSizeInfo(tableEstimateBytes, avgTableRowSizeBytes); - final AirbyteStreamNameNamespacePair namespacePair = - new AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()); - tableSizeInfoMap.put(namespacePair, tableSizeInfo); - } - } catch (final Exception e) { - LOGGER.warn("Error occurred while attempting to estimate sync size", e); - } - }); - return tableSizeInfoMap; - } - - /** - * Iterates through each stream and find the max cursor value and the record count which has that - * value based on each cursor field provided by the customer per stream This information is saved in - * a Hashmap with the mapping being the AirbyteStreamNameNamespacepair -> CursorBasedStatus - * - * @param database the source db - * @param streams streams to be synced - * @param stateManager stream stateManager - * @return Map of streams to statuses - */ - public static Map getCursorBasedSyncStatusForStreams(final JdbcDatabase database, - final List streams, - final StateManager stateManager, - final String quoteString) { - - final Map cursorBasedStatusMap = new HashMap<>(); - streams.forEach(stream -> { - try { - final String name = stream.getStream().getName(); - final String namespace = stream.getStream().getNamespace(); - final String fullTableName = - getFullyQualifiedTableNameWithQuoting(namespace, name, quoteString); - - final Optional cursorInfoOptional = - stateManager.getCursorInfo(new io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair(name, namespace)); - if (cursorInfoOptional.isEmpty()) { - throw new RuntimeException(String.format("Stream %s was not provided with an appropriate cursor", stream.getStream().getName())); - } - - LOGGER.info("Querying max cursor value for {}.{}", namespace, name); - - final String cursorField = cursorInfoOptional.get().getCursorField(); - LOGGER.info("cursor field", cursorField); - final String quotedCursorField = getIdentifierWithQuoting(cursorField, quoteString); - final String cursorBasedSyncStatusQuery = String.format(MAX_CURSOR_VALUE_QUERY, - quotedCursorField, - fullTableName, - quotedCursorField, - quotedCursorField, - fullTableName); - final List jsonNodes = database.bufferedResultSetQuery(conn -> conn.prepareStatement(cursorBasedSyncStatusQuery).executeQuery(), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - final CursorBasedStatus cursorBasedStatus = new CursorBasedStatus(); - cursorBasedStatus.setStateType(StateType.CURSOR_BASED); - cursorBasedStatus.setVersion(2L); - cursorBasedStatus.setStreamName(name); - cursorBasedStatus.setStreamNamespace(namespace); - cursorBasedStatus.setCursorField(ImmutableList.of(cursorField)); - - if (!jsonNodes.isEmpty()) { - final JsonNode result = jsonNodes.get(0); - cursorBasedStatus.setCursor(result.get(cursorField).asText()); - cursorBasedStatus.setCursorRecordCount((long) jsonNodes.size()); - } - - cursorBasedStatusMap.put(new io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair(name, namespace), cursorBasedStatus); - } catch (final SQLException e) { - throw new RuntimeException(e); - } - }); - - return cursorBasedStatusMap; - } - - private static List getTableEstimate(final JdbcDatabase database, final String namespace, final String name) - throws SQLException { - // Construct the table estimate query. - final String tableEstimateQuery = - String.format(TABLE_ESTIMATE_QUERY, TABLE_SIZE_BYTES_COL, AVG_ROW_LENGTH, namespace, name); - LOGGER.info("Querying for table size estimate: {}", tableEstimateQuery); - final List jsonNodes = database.bufferedResultSetQuery(conn -> conn.createStatement().executeQuery(tableEstimateQuery), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - Preconditions.checkState(jsonNodes.size() == 1); - return jsonNodes; - } - - public static void logStreamSyncStatus(final List streams, final String syncType) { - if (streams.isEmpty()) { - LOGGER.info("No Streams will be synced via {}.", syncType); - } else { - LOGGER.info("Streams to be synced via {} : {}", syncType, streams.size()); - LOGGER.info("Streams: {}", prettyPrintConfiguredAirbyteStreamList(streams)); - } - } - - public static String prettyPrintConfiguredAirbyteStreamList(final List streamList) { - return streamList.stream().map(s -> "%s.%s".formatted(s.getStream().getNamespace(), s.getStream().getName())).collect(Collectors.joining(", ")); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java deleted file mode 100644 index 2c3eb8f78ef7..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSource.java +++ /dev/null @@ -1,669 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static io.airbyte.cdk.db.jdbc.JdbcUtils.EQUALS; -import static io.airbyte.cdk.integrations.debezium.AirbyteDebeziumHandler.isAnyStreamIncrementalSyncMode; -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_DELETED_AT; -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_UPDATED_AT; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcDataSourceUtils.DEFAULT_JDBC_PARAMETERS_DELIMITER; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcDataSourceUtils.assertCustomParametersDontOverwriteDefaultParameters; -import static io.airbyte.integrations.source.mysql.MySqlQueryUtils.getCursorBasedSyncStatusForStreams; -import static io.airbyte.integrations.source.mysql.MySqlQueryUtils.getTableSizeInfoForStreams; -import static io.airbyte.integrations.source.mysql.MySqlQueryUtils.logStreamSyncStatus; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.convertNameNamespacePairFromV0; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.filterStreamInIncrementalMode; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.getMySqlFullRefreshInitialLoadHandler; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.getMySqlInitialLoadGlobalStateManager; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.initPairToPrimaryKeyInfoMap; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.isSavedOffsetStillPresentOnServer; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.streamsForInitialPrimaryKeyLoad; -import static java.util.stream.Collectors.toList; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.factory.DataSourceFactory; -import io.airbyte.cdk.db.factory.DatabaseDriver; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.db.jdbc.StreamingJdbcDatabase; -import io.airbyte.cdk.integrations.base.IntegrationRunner; -import io.airbyte.cdk.integrations.base.Source; -import io.airbyte.cdk.integrations.base.adaptive.AdaptiveSourceRunner; -import io.airbyte.cdk.integrations.base.ssh.SshWrappedSource; -import io.airbyte.cdk.integrations.debezium.internals.RecordWaitTimeUtil; -import io.airbyte.cdk.integrations.source.jdbc.AbstractJdbcSource; -import io.airbyte.cdk.integrations.source.jdbc.JdbcDataSourceUtils; -import io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils; -import io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.SslMode; -import io.airbyte.cdk.integrations.source.relationaldb.DbSourceDiscoverUtil; -import io.airbyte.cdk.integrations.source.relationaldb.InitialLoadHandler; -import io.airbyte.cdk.integrations.source.relationaldb.TableInfo; -import io.airbyte.cdk.integrations.source.relationaldb.state.NonResumableStateMessageProducer; -import io.airbyte.cdk.integrations.source.relationaldb.state.SourceStateMessageProducer; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateGeneratorUtils; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManager; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManagerFactory; -import io.airbyte.cdk.integrations.source.relationaldb.streamstatus.StreamStatusTraceEmitterIterator; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.commons.functional.CheckedConsumer; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.map.MoreMaps; -import io.airbyte.commons.stream.AirbyteStreamStatusHolder; -import io.airbyte.commons.util.AutoCloseableIterator; -import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mysql.cdc.CdcConfigurationHelper; -import io.airbyte.integrations.source.mysql.cursor_based.MySqlCursorBasedStateManager; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadGlobalStateManager; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadHandler; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStateManager; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStreamStateManager; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.CursorBasedStreams; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.InitialLoadStreams; -import io.airbyte.integrations.source.mysql.internal.models.CursorBasedStatus; -import io.airbyte.protocol.models.CommonField; -import io.airbyte.protocol.models.v0.*; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus.Status; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.sql.SQLException; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.sql.DataSource; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlSource extends AbstractJdbcSource implements Source { - - public static final String TUNNEL_METHOD = "tunnel_method"; - public static final String NO_TUNNEL = "NO_TUNNEL"; - public static final String SSL_MODE = "ssl_mode"; - private static final String MODE = "mode"; - public static final String SSL_MODE_PREFERRED = "preferred"; - public static final String SSL_MODE_REQUIRED = "required"; - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlSource.class); - private static final int INTERMEDIATE_STATE_EMISSION_FREQUENCY = 10_000; - public static final String NULL_CURSOR_VALUE_WITH_SCHEMA_QUERY = - """ - SELECT (EXISTS (SELECT * from `%s`.`%s` where `%s` IS NULL LIMIT 1)) AS %s - """; - public static final String NULL_CURSOR_VALUE_WITHOUT_SCHEMA_QUERY = - """ - SELECT (EXISTS (SELECT * from %s where `%s` IS NULL LIMIT 1)) AS %s - """; - public static final String DESCRIBE_TABLE_WITHOUT_SCHEMA_QUERY = - """ - DESCRIBE %s - """; - public static final String DESCRIBE_TABLE_WITH_SCHEMA_QUERY = - """ - DESCRIBE `%s`.`%s` - """; - - public static final String DRIVER_CLASS = DatabaseDriver.MYSQL.getDriverClassName(); - public static final String CDC_LOG_FILE = "_ab_cdc_log_file"; - public static final String CDC_LOG_POS = "_ab_cdc_log_pos"; - public static final String CDC_DEFAULT_CURSOR = "_ab_cdc_cursor"; - - public static Source sshWrappedSource(final MySqlSource source) { - return new SshWrappedSource(source, JdbcUtils.HOST_LIST_KEY, JdbcUtils.PORT_LIST_KEY); - } - - private ConnectorSpecification getCloudDeploymentSpec(final ConnectorSpecification originalSpec) { - final ConnectorSpecification spec = Jsons.clone(originalSpec); - // Remove the SSL options - ((ObjectNode) spec.getConnectionSpecification().get("properties")).remove(JdbcUtils.SSL_KEY); - // Set SSL_MODE to required by default - ((ObjectNode) spec.getConnectionSpecification().get("properties").get(SSL_MODE)).put("default", SSL_MODE_REQUIRED); - return spec; - } - - @Override - public @NotNull ConnectorSpecification spec() throws Exception { - if (cloudDeploymentMode()) { - return getCloudDeploymentSpec(super.spec()); - } - return super.spec(); - } - - @Override - public AirbyteConnectionStatus check(final @NotNull JsonNode config) throws Exception { - // #15808 Disallow connecting to db with disable, prefer or allow SSL mode when connecting directly - // and not over SSH tunnel - if (cloudDeploymentMode()) { - if (config.has(TUNNEL_METHOD) - && config.get(TUNNEL_METHOD).has(TUNNEL_METHOD) - && config.get(TUNNEL_METHOD).get(TUNNEL_METHOD).asText().equals(NO_TUNNEL)) { - // If no SSH tunnel - if (config.has(SSL_MODE) && config.get(SSL_MODE).has(MODE)) { - if (Set.of(SSL_MODE_PREFERRED).contains(config.get(SSL_MODE).get(MODE).asText())) { - // Fail in case SSL mode is preferred - return new AirbyteConnectionStatus() - .withStatus(Status.FAILED) - .withMessage( - "Unsecured connection not allowed. If no SSH Tunnel set up, please use one of the following SSL modes: required, verify-ca, verify-identity"); - } - } - } - } - return super.check(config); - } - - public MySqlSource() { - super(DRIVER_CLASS, MySqlStreamingQueryConfig::new, new MySqlSourceOperations()); - } - - @Override - public boolean supportResumableFullRefresh(final JdbcDatabase database, final ConfiguredAirbyteStream airbyteStream) { - if (airbyteStream.getStream() != null && airbyteStream.getStream().getSourceDefinedPrimaryKey() != null - && !airbyteStream.getStream().getSourceDefinedPrimaryKey().isEmpty()) { - return true; - } - - return false; - } - - private MySqlInitialLoadStateManager initialLoadStateManager = null; - private boolean isSavedOffsetStillPresentOnServer = false; - - @Override - protected void initializeForStateManager(final JdbcDatabase database, - final @NotNull ConfiguredAirbyteCatalog catalog, - final @NotNull Map>> tableNameToTable, - final @NotNull StateManager stateManager) { - if (initialLoadStateManager != null) { - return; - } - final var sourceConfig = database.getSourceConfig(); - - if (isCdc(sourceConfig)) { - isSavedOffsetStillPresentOnServer = isSavedOffsetStillPresentOnServer(database, catalog, stateManager); - initialLoadStateManager = getMySqlInitialLoadGlobalStateManager(database, catalog, stateManager, tableNameToTable, getQuoteString(), - isSavedOffsetStillPresentOnServer); - } else { - final MySqlCursorBasedStateManager cursorBasedStateManager = new MySqlCursorBasedStateManager(stateManager.getRawStateMessages(), catalog); - final InitialLoadStreams initialLoadStreams = streamsForInitialPrimaryKeyLoad(cursorBasedStateManager, catalog); - initialLoadStateManager = - new MySqlInitialLoadStreamStateManager(catalog, initialLoadStreams, - initPairToPrimaryKeyInfoMap(database, catalog, tableNameToTable, getQuoteString())); - } - } - - @Override - public InitialLoadHandler getInitialLoadHandler(final JdbcDatabase database, - final ConfiguredAirbyteStream stream, - final ConfiguredAirbyteCatalog catalog, - final StateManager stateManager) { - - var sourceConfig = database.getSourceConfig(); - - if (isCdc(sourceConfig)) { - return getMySqlFullRefreshInitialLoadHandler(database, catalog, (MySqlInitialLoadGlobalStateManager) initialLoadStateManager, stateManager, - stream, Instant.now(), getQuoteString(), isSavedOffsetStillPresentOnServer) - .get(); - } else { - return new MySqlInitialLoadHandler(sourceConfig, database, new MySqlSourceOperations(), getQuoteString(), initialLoadStateManager, - Optional.empty(), - getTableSizeInfoForStreams(database, catalog.getStreams(), getQuoteString())); - } - } - - @Override - protected SourceStateMessageProducer getSourceStateProducerForNonResumableFullRefreshStream(final JdbcDatabase database) { - return new NonResumableStateMessageProducer<>(isCdc(database.getSourceConfig()), initialLoadStateManager); - } - - private static AirbyteStream overrideSyncModes(final AirbyteStream stream) { - return stream.withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)); - } - - private static AirbyteStream removeIncrementalWithoutPk(final AirbyteStream stream) { - if (stream.getSourceDefinedPrimaryKey().isEmpty()) { - stream.getSupportedSyncModes().remove(SyncMode.INCREMENTAL); - } - - return stream; - } - - private static AirbyteStream setIncrementalToSourceDefined(final AirbyteStream stream) { - if (stream.getSupportedSyncModes().contains(SyncMode.INCREMENTAL)) { - stream.setSourceDefinedCursor(true); - } - - return stream; - } - - /* - * To prepare for Destination v2, cdc streams must have a default cursor field Cursor format: the - * airbyte [emittedAt(converted to nano seconds)] + [sync wide record counter] - */ - private static AirbyteStream setDefaultCursorFieldForCdc(final AirbyteStream stream) { - if (stream.getSupportedSyncModes().contains(SyncMode.INCREMENTAL)) { - stream.setDefaultCursorField(ImmutableList.of(CDC_DEFAULT_CURSOR)); - } - return stream; - } - - // Note: in place mutation. - private static AirbyteStream addCdcMetadataColumns(final AirbyteStream stream) { - final ObjectNode jsonSchema = (ObjectNode) stream.getJsonSchema(); - final ObjectNode properties = (ObjectNode) jsonSchema.get("properties"); - - final JsonNode numberType = Jsons.jsonNode(ImmutableMap.of("type", "number")); - final JsonNode airbyteIntegerType = Jsons.jsonNode(ImmutableMap.of("type", "number", "airbyte_type", "integer")); - final JsonNode stringType = Jsons.jsonNode(ImmutableMap.of("type", "string")); - properties.set(CDC_LOG_FILE, stringType); - properties.set(CDC_LOG_POS, numberType); - properties.set(CDC_UPDATED_AT, stringType); - properties.set(CDC_DELETED_AT, stringType); - properties.set(CDC_DEFAULT_CURSOR, airbyteIntegerType); - - return stream; - } - - @Override - public List> getCheckOperations(final JsonNode config) throws Exception { - final List> checkOperations = new ArrayList<>(super.getCheckOperations(config)); - if (isCdc(config)) { - checkOperations.addAll(CdcConfigurationHelper.getCheckOperations()); - - checkOperations.add(database -> { - RecordWaitTimeUtil.checkFirstRecordWaitTime(config); - CdcConfigurationHelper.checkServerTimeZoneConfig(config); - }); - } - return checkOperations; - } - - @Override - public AirbyteCatalog discover(final JsonNode config) { - final AirbyteCatalog catalog = super.discover(config); - - if (isCdc(config)) { - final List streams = catalog.getStreams().stream() - .map(MySqlSource::overrideSyncModes) - .map(MySqlSource::removeIncrementalWithoutPk) - .map(MySqlSource::setIncrementalToSourceDefined) - .map(MySqlSource::setDefaultCursorFieldForCdc) - .map(MySqlSource::addCdcMetadataColumns) - .collect(toList()); - - catalog.setStreams(streams); - } - - return catalog; - } - - @Override - public Collection> readStreams(final JsonNode config, - final ConfiguredAirbyteCatalog catalog, - final JsonNode state) - throws Exception { - final AirbyteStateType supportedStateType = getSupportedStateType(config); - final StateManager stateManager = - StateManagerFactory.createStateManager(supportedStateType, - StateGeneratorUtils.deserializeInitialState(state, supportedStateType), catalog); - final Instant emittedAt = Instant.now(); - - final JdbcDatabase database = createDatabase(config); - - logPreSyncDebugData(database, catalog); - - final Map>> fullyQualifiedTableNameToInfo = - discoverWithoutSystemTables(database) - .stream() - .collect(Collectors.toMap(t -> String.format("%s.%s", t.getNameSpace(), t.getName()), - Function - .identity())); - - validateCursorFieldForIncrementalTables(fullyQualifiedTableNameToInfo, catalog, database); - - initializeForStateManager(database, catalog, fullyQualifiedTableNameToInfo, stateManager); - - DbSourceDiscoverUtil.logSourceSchemaChange(fullyQualifiedTableNameToInfo, catalog, this::getAirbyteType); - - final List> incrementalIterators = - getIncrementalIterators(database, catalog, fullyQualifiedTableNameToInfo, stateManager, - emittedAt); - final List> fullRefreshIterators = - getFullRefreshIterators(database, catalog, fullyQualifiedTableNameToInfo, stateManager, - emittedAt); - final List> iteratorList = Stream - .of(incrementalIterators, fullRefreshIterators) - .flatMap(Collection::stream) - .collect(Collectors.toList()); - - return iteratorList; - } - - @Override - protected void logPreSyncDebugData(final JdbcDatabase database, final ConfiguredAirbyteCatalog catalog) - throws SQLException { - super.logPreSyncDebugData(database, catalog); - final Set streamNames = new HashSet<>(); - for (final ConfiguredAirbyteStream stream : catalog.getStreams()) { - streamNames.add(stream.getStream().getName()); - } - final Set storageEngines = MySqlQueryUtils.getStorageEngines(database, streamNames); - LOGGER.info(String.format("Detected the following storage engines for MySQL: %s", storageEngines.toString())); - } - - @Override - public JsonNode toDatabaseConfig(final JsonNode config) { - final String encodedDatabaseName = URLEncoder.encode(config.get(JdbcUtils.DATABASE_KEY).asText(), StandardCharsets.UTF_8); - final StringBuilder jdbcUrl = new StringBuilder(String.format("jdbc:mysql://%s:%s/%s", - config.get(JdbcUtils.HOST_KEY).asText(), - config.get(JdbcUtils.PORT_KEY).asText(), - encodedDatabaseName)); - - // To fetch the result in batches, the "useCursorFetch=true" must be set. - // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-implementation-notes.html. - // When using this approach MySql creates a temporary table which may have some effect on db - // performance. - jdbcUrl.append("?useCursorFetch=true"); - // What should happen when the driver encounters DATETIME values that are composed entirely of zeros - // https://dev.mysql.com/doc/connector-j/8.1/en/connector-j-connp-props-datetime-types-processing.html#cj-conn-prop_zeroDateTimeBehavior - jdbcUrl.append("&zeroDateTimeBehavior=CONVERT_TO_NULL"); - // ensure the return tinyint(1) is boolean - jdbcUrl.append("&tinyInt1isBit=true"); - // ensure the return year value is a Date; see the rationale - // in the setJsonField method in MySqlSourceOperations.java - jdbcUrl.append("&yearIsDateType=false"); - if (config.get(JdbcUtils.JDBC_URL_PARAMS_KEY) != null && !config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText().isEmpty()) { - jdbcUrl.append(JdbcUtils.AMPERSAND).append(config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText()); - } - final Map sslParameters = JdbcSSLConnectionUtils.parseSSLConfig(config); - jdbcUrl.append(JdbcUtils.AMPERSAND).append(toJDBCQueryParams(sslParameters)); - - final ImmutableMap.Builder configBuilder = ImmutableMap.builder() - .put(JdbcUtils.USERNAME_KEY, config.get(JdbcUtils.USERNAME_KEY).asText()) - .put(JdbcUtils.JDBC_URL_KEY, jdbcUrl.toString()); - - configBuilder.putAll(sslParameters); - - if (config.has(JdbcUtils.PASSWORD_KEY)) { - configBuilder.put(JdbcUtils.PASSWORD_KEY, config.get(JdbcUtils.PASSWORD_KEY).asText()); - } - return Jsons.jsonNode(configBuilder.build()); - } - - /** - * Generates SSL related query parameters from map of parsed values. - * - * @apiNote Different connector may need an override for specific implementation - * @param sslParams - * @return SSL portion of JDBC question params or and empty string - */ - private String toJDBCQueryParams(final Map sslParams) { - return Objects.isNull(sslParams) ? "" - : sslParams.entrySet() - .stream() - .map((entry) -> { - if (entry.getKey().equals(SSL_MODE)) { - return entry.getKey() + EQUALS + toSslJdbcParam(SslMode.valueOf(entry.getValue())); - } else { - return entry.getKey() + EQUALS + entry.getValue(); - } - }) - .collect(Collectors.joining(JdbcUtils.AMPERSAND)); - } - - private static boolean isCdc(final JsonNode config) { - if (config.hasNonNull("replication_method")) { - if (config.get("replication_method").isTextual()) { - return ReplicationMethod.valueOf(config.get("replication_method").asText()) - .equals(ReplicationMethod.CDC); - } else if (config.get("replication_method").isObject()) { - return config.get("replication_method").get("method").asText() - .equals(ReplicationMethod.CDC.name()); - } - } - return false; - } - - @Override - protected AirbyteStateType getSupportedStateType(final JsonNode config) { - return isCdc(config) ? AirbyteStateType.GLOBAL : AirbyteStateType.STREAM; - } - - @Override - public List> getIncrementalIterators(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final Map>> tableNameToTable, - final StateManager stateManager, - final Instant emittedAt) { - - final JsonNode sourceConfig = database.getSourceConfig(); - if (isCdc(sourceConfig) && isAnyStreamIncrementalSyncMode(catalog)) { - LOGGER.info("Using PK + CDC"); - return MySqlInitialReadUtil.getCdcReadIterators(database, catalog, tableNameToTable, stateManager, - (MySqlInitialLoadGlobalStateManager) initialLoadStateManager, emittedAt, - getQuoteString(), isSavedOffsetStillPresentOnServer); - } else { - if (isAnyStreamIncrementalSyncMode(catalog)) { - LOGGER.info("Syncing via Primary Key"); - final MySqlCursorBasedStateManager cursorBasedStateManager = new MySqlCursorBasedStateManager(stateManager.getRawStateMessages(), catalog); - final InitialLoadStreams initialLoadStreams = - filterStreamInIncrementalMode(streamsForInitialPrimaryKeyLoad(cursorBasedStateManager, catalog)); - final Map pairToCursorBasedStatus = - getCursorBasedSyncStatusForStreams(database, initialLoadStreams.streamsForInitialLoad(), stateManager, getQuoteString()); - final CursorBasedStreams cursorBasedStreams = - new CursorBasedStreams(MySqlInitialReadUtil.identifyStreamsForCursorBased(catalog, initialLoadStreams.streamsForInitialLoad()), - pairToCursorBasedStatus); - - logStreamSyncStatus(initialLoadStreams.streamsForInitialLoad(), "Primary Key"); - logStreamSyncStatus(cursorBasedStreams.streamsForCursorBased(), "Cursor"); - - final MySqlInitialLoadHandler initialLoadHandler = - new MySqlInitialLoadHandler(sourceConfig, database, new MySqlSourceOperations(), getQuoteString(), initialLoadStateManager, - Optional.of(namespacePair -> Jsons.jsonNode(pairToCursorBasedStatus.get(convertNameNamespacePairFromV0(namespacePair)))), - getTableSizeInfoForStreams(database, catalog.getStreams(), getQuoteString())); - // Cursor based incremental iterators are decorated with start and complete status traces - final List> initialLoadIterator = new ArrayList<>(initialLoadHandler.getIncrementalIterators( - new ConfiguredAirbyteCatalog().withStreams(initialLoadStreams.streamsForInitialLoad()), - tableNameToTable, - emittedAt, true, true, Optional.empty())); - - // Build Cursor based iterator - final List> cursorBasedIterator = - new ArrayList<>(super.getIncrementalIterators(database, - new ConfiguredAirbyteCatalog().withStreams( - cursorBasedStreams.streamsForCursorBased()), - tableNameToTable, - cursorBasedStateManager, emittedAt)); - - return Stream.of(initialLoadIterator, cursorBasedIterator).flatMap(Collection::stream).collect(Collectors.toList()); - } - } - - LOGGER.info("using CDC: {}", false); - return super.getIncrementalIterators(database, catalog, tableNameToTable, stateManager, - emittedAt); - } - - @Override - public Set getExcludedInternalNameSpaces() { - return Set.of( - "information_schema", - "mysql", - "performance_schema", - "sys"); - } - - @Override - protected boolean verifyCursorColumnValues(final JdbcDatabase database, final String schema, final String tableName, final String columnName) - throws SQLException { - final boolean nullValExist; - final String resultColName = "nullValue"; - final String descQuery = schema == null || schema.isBlank() - ? String.format(DESCRIBE_TABLE_WITHOUT_SCHEMA_QUERY, tableName) - : String.format(DESCRIBE_TABLE_WITH_SCHEMA_QUERY, schema, tableName); - final List tableRows = database.bufferedResultSetQuery(conn -> conn.createStatement() - .executeQuery(descQuery), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - - Optional field = Optional.empty(); - String nullableColumnName = ""; - for (final JsonNode tableRow : tableRows) { - LOGGER.info("MySQL Table Structure {}, {}, {}", tableRow.toString(), schema, tableName); - if (tableRow.get("Field") != null && tableRow.get("Field").asText().equalsIgnoreCase(columnName)) { - field = Optional.of(tableRow); - nullableColumnName = "Null"; - } else if (tableRow.get("COLUMN_NAME") != null && tableRow.get("COLUMN_NAME").asText().equalsIgnoreCase(columnName)) { - field = Optional.of(tableRow); - nullableColumnName = "IS_NULLABLE"; - } - } - if (field.isPresent()) { - final JsonNode jsonNode = field.get(); - final JsonNode isNullable = jsonNode.get(nullableColumnName); - if (isNullable != null) { - if (isNullable.asText().equalsIgnoreCase("YES")) { - final String query = schema == null || schema.isBlank() - ? String.format(NULL_CURSOR_VALUE_WITHOUT_SCHEMA_QUERY, - tableName, columnName, resultColName) - : String.format(NULL_CURSOR_VALUE_WITH_SCHEMA_QUERY, - schema, tableName, columnName, resultColName); - - LOGGER.debug("null value query: {}", query); - final List jsonNodes = database.bufferedResultSetQuery(conn -> conn.createStatement().executeQuery(query), - resultSet -> JdbcUtils.getDefaultSourceOperations().rowToJson(resultSet)); - Preconditions.checkState(jsonNodes.size() == 1); - nullValExist = convertToBoolean(jsonNodes.get(0).get(resultColName).toString()); - LOGGER.info("null cursor value for MySQL source : {}, shema {} , tableName {}, columnName {} ", nullValExist, schema, tableName, - columnName); - } - } - } - // return !nullValExist; - // will enable after we have sent comms to users this affects - return true; - } - - private boolean convertToBoolean(final String value) { - return "1".equalsIgnoreCase(value) || "true".equalsIgnoreCase(value); - } - - private boolean cloudDeploymentMode() { - return AdaptiveSourceRunner.CLOUD_MODE.equalsIgnoreCase(getFeatureFlags().deploymentMode()); - } - - @Override - protected int getStateEmissionFrequency() { - return INTERMEDIATE_STATE_EMISSION_FREQUENCY; - } - - public static String toSslJdbcParam(final SslMode sslMode) { - final var result = switch (sslMode) { - case DISABLED, PREFERRED, REQUIRED, VERIFY_CA, VERIFY_IDENTITY -> sslMode.name(); - default -> throw new IllegalArgumentException("unexpected ssl mode"); - }; - return result; - } - - @Override - public JdbcDatabase createDatabase(final JsonNode sourceConfig) throws SQLException { - // return super.createDatabase(sourceConfig, this::getConnectionProperties); - final JsonNode jdbcConfig = toDatabaseConfig(sourceConfig); - final Map connectionProperties = this.getConnectionProperties(sourceConfig); - // Create the data source - final DataSource dataSource = DataSourceFactory.create( - jdbcConfig.has(JdbcUtils.USERNAME_KEY) ? jdbcConfig.get(JdbcUtils.USERNAME_KEY).asText() : null, - jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null, - driverClassName, - jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(), - connectionProperties, - getConnectionTimeout(connectionProperties, driverClassName)); - // Record the data source so that it can be closed. - dataSources.add(dataSource); - - final JdbcDatabase database = new StreamingJdbcDatabase( - dataSource, - sourceOperations, - streamingQueryConfigProvider); - - setQuoteString((getQuoteString() == null ? database.getMetaData().getIdentifierQuoteString() : getQuoteString())); - database.setSourceConfig(sourceConfig); - database.setDatabaseConfig(jdbcConfig); - return database; - } - - public Map getConnectionProperties(final JsonNode config) { - final Map customProperties = - config.has(JdbcUtils.JDBC_URL_PARAMS_KEY) - ? parseJdbcParameters(config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText(), DEFAULT_JDBC_PARAMETERS_DELIMITER) - : new HashMap<>(); - final Map defaultProperties = JdbcDataSourceUtils.getDefaultConnectionProperties(config); - assertCustomParametersDontOverwriteDefaultParameters(customProperties, defaultProperties); - return MoreMaps.merge(customProperties, defaultProperties); - } - - public static Map parseJdbcParameters(final String jdbcPropertiesString, final String delimiter) { - final Map parameters = new HashMap<>(); - if (!jdbcPropertiesString.isBlank()) { - final String[] keyValuePairs = jdbcPropertiesString.split(delimiter); - for (final String kv : keyValuePairs) { - final String[] split = kv.split("="); - if (split.length == 2) { - parameters.put(split[0], split[1]); - } else if (split.length == 3 && kv.contains("sessionVariables")) { - parameters.put(split[0], split[1] + "=" + split[2]); - } else { - throw new ConfigErrorException( - "jdbc_url_params must be formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). Got " - + jdbcPropertiesString); - } - } - } - return parameters; - } - - public static void main(final String[] args) throws Exception { - final Source source = MySqlSource.sshWrappedSource(new MySqlSource()); - final MySqlSourceExceptionHandler exceptionHandler = new MySqlSourceExceptionHandler(); - LOGGER.info("starting source: {}", MySqlSource.class); - new IntegrationRunner(source).run(args, exceptionHandler); - LOGGER.info("completed source: {}", MySqlSource.class); - } - - public enum ReplicationMethod { - STANDARD, - CDC - } - - @NotNull - @Override - public AutoCloseableIterator augmentWithStreamStatus(@NotNull final ConfiguredAirbyteStream airbyteStream, - @NotNull final AutoCloseableIterator streamItrator) { - final var pair = - new io.airbyte.protocol.models.AirbyteStreamNameNamespacePair(airbyteStream.getStream().getName(), airbyteStream.getStream().getNamespace()); - final var starterStatus = - new StreamStatusTraceEmitterIterator(new AirbyteStreamStatusHolder(pair, AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.STARTED)); - final var completeStatus = - new StreamStatusTraceEmitterIterator(new AirbyteStreamStatusHolder(pair, AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.COMPLETE)); - return AutoCloseableIterators.concatWithEagerClose(starterStatus, streamItrator, completeStatus); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSourceOperations.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSourceOperations.java deleted file mode 100644 index c322ebb2ed60..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSourceOperations.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static com.mysql.cj.MysqlType.BIGINT; -import static com.mysql.cj.MysqlType.BIGINT_UNSIGNED; -import static com.mysql.cj.MysqlType.DATE; -import static com.mysql.cj.MysqlType.DATETIME; -import static com.mysql.cj.MysqlType.DECIMAL; -import static com.mysql.cj.MysqlType.DECIMAL_UNSIGNED; -import static com.mysql.cj.MysqlType.DOUBLE; -import static com.mysql.cj.MysqlType.DOUBLE_UNSIGNED; -import static com.mysql.cj.MysqlType.FLOAT; -import static com.mysql.cj.MysqlType.FLOAT_UNSIGNED; -import static com.mysql.cj.MysqlType.INT; -import static com.mysql.cj.MysqlType.INT_UNSIGNED; -import static com.mysql.cj.MysqlType.LONGTEXT; -import static com.mysql.cj.MysqlType.MEDIUMINT; -import static com.mysql.cj.MysqlType.MEDIUMINT_UNSIGNED; -import static com.mysql.cj.MysqlType.MEDIUMTEXT; -import static com.mysql.cj.MysqlType.SMALLINT; -import static com.mysql.cj.MysqlType.SMALLINT_UNSIGNED; -import static com.mysql.cj.MysqlType.TEXT; -import static com.mysql.cj.MysqlType.TIME; -import static com.mysql.cj.MysqlType.TIMESTAMP; -import static com.mysql.cj.MysqlType.TINYINT; -import static com.mysql.cj.MysqlType.TINYINT_UNSIGNED; -import static com.mysql.cj.MysqlType.TINYTEXT; -import static com.mysql.cj.MysqlType.VARCHAR; -import static com.mysql.cj.MysqlType.YEAR; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_COLUMN_NAME; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_COLUMN_SIZE; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_COLUMN_TYPE; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_COLUMN_TYPE_NAME; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_DECIMAL_DIGITS; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_SCHEMA_NAME; -import static io.airbyte.cdk.db.jdbc.JdbcConstants.INTERNAL_TABLE_NAME; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.NullNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.mysql.cj.MysqlType; -import com.mysql.cj.jdbc.result.ResultSetMetaData; -import com.mysql.cj.result.Field; -import io.airbyte.cdk.db.SourceOperations; -import io.airbyte.cdk.db.jdbc.AbstractJdbcCompatibleSourceOperations; -import io.airbyte.cdk.db.jdbc.AirbyteRecordData; -import io.airbyte.integrations.source.mysql.initialsync.CdcMetadataInjector; -import io.airbyte.protocol.models.JsonSchemaType; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.format.DateTimeParseException; -import java.util.Optional; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlSourceOperations extends AbstractJdbcCompatibleSourceOperations implements SourceOperations { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlSourceOperations.class); - private static final Set ALLOWED_CURSOR_TYPES = Set.of(TINYINT, TINYINT_UNSIGNED, SMALLINT, - SMALLINT_UNSIGNED, MEDIUMINT, MEDIUMINT_UNSIGNED, INT, INT_UNSIGNED, BIGINT, BIGINT_UNSIGNED, - FLOAT, FLOAT_UNSIGNED, DOUBLE, DOUBLE_UNSIGNED, DECIMAL, DECIMAL_UNSIGNED, DATE, DATETIME, TIMESTAMP, - TIME, YEAR, VARCHAR, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT); - - private final Optional metadataInjector; - - public MySqlSourceOperations() { - super(); - this.metadataInjector = Optional.empty(); - } - - public MySqlSourceOperations(final Optional metadataInjector) { - super(); - this.metadataInjector = metadataInjector; - } - - @Override - public AirbyteRecordData convertDatabaseRowToAirbyteRecordData(final ResultSet queryContext) throws SQLException { - final AirbyteRecordData recordData = super.convertDatabaseRowToAirbyteRecordData(queryContext); - final ObjectNode jsonNode = (ObjectNode) recordData.rawRowData(); - if (!metadataInjector.isPresent()) { - return recordData; - } - metadataInjector.get().inject(jsonNode); - return new AirbyteRecordData(jsonNode, recordData.meta()); - } - - /** - * @param colIndex 1-based column index. - */ - @Override - public void copyToJsonField(final ResultSet resultSet, final int colIndex, final ObjectNode json) throws SQLException { - final ResultSetMetaData metaData = (ResultSetMetaData) resultSet.getMetaData(); - final Field field = metaData.getFields()[colIndex - 1]; - final String columnName = field.getName(); - final MysqlType columnType = field.getMysqlType(); - - // Attempt to access the column. this allows us to know if it is null before we do - // type-specific parsing. If the column is null, we will populate the null value and skip attempting - // to - // parse the column value. - resultSet.getObject(colIndex); - if (resultSet.wasNull()) { - json.putNull(columnName); - } else { - // https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-type-conversions.html - switch (columnType) { - case BIT -> { - if (field.getLength() == 1L) { - // BIT(1) is boolean - putBoolean(json, columnName, resultSet, colIndex); - } else { - putBinary(json, columnName, resultSet, colIndex); - } - } - case BOOLEAN -> putBoolean(json, columnName, resultSet, colIndex); - case TINYINT -> { - if (field.getLength() == 1L) { - // TINYINT(1) is boolean - putBoolean(json, columnName, resultSet, colIndex); - } else { - putShortInt(json, columnName, resultSet, colIndex); - } - } - case TINYINT_UNSIGNED, YEAR -> putShortInt(json, columnName, resultSet, colIndex); - case SMALLINT, SMALLINT_UNSIGNED, MEDIUMINT, MEDIUMINT_UNSIGNED -> putInteger(json, columnName, resultSet, colIndex); - case INT, INT_UNSIGNED -> { - if (field.isUnsigned()) { - putBigInt(json, columnName, resultSet, colIndex); - } else { - putInteger(json, columnName, resultSet, colIndex); - } - } - case BIGINT, BIGINT_UNSIGNED -> putBigInt(json, columnName, resultSet, colIndex); - case FLOAT, FLOAT_UNSIGNED -> putFloat(json, columnName, resultSet, colIndex); - case DOUBLE, DOUBLE_UNSIGNED -> putDouble(json, columnName, resultSet, colIndex); - case DECIMAL, DECIMAL_UNSIGNED -> { - if (field.getDecimals() == 0) { - putBigInt(json, columnName, resultSet, colIndex); - } else { - putBigDecimal(json, columnName, resultSet, colIndex); - } - } - case DATE -> putDate(json, columnName, resultSet, colIndex); - case DATETIME -> putTimestamp(json, columnName, resultSet, colIndex); - case TIMESTAMP -> putTimestampWithTimezone(json, columnName, resultSet, colIndex); - case TIME -> putTime(json, columnName, resultSet, colIndex); - case CHAR, VARCHAR -> putString(json, columnName, resultSet, colIndex); - case TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, BINARY, VARBINARY, GEOMETRY -> putBinary(json, columnName, resultSet, colIndex); - case TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, JSON, ENUM, SET -> putString(json, columnName, resultSet, colIndex); - case NULL -> json.set(columnName, NullNode.instance); - default -> putDefault(json, columnName, resultSet, colIndex); - } - } - } - - /** - * MySQL boolean is equivalent to tinyint(1). - */ - @Override - protected void putBoolean(final ObjectNode node, final String columnName, final ResultSet resultSet, final int index) throws SQLException { - node.put(columnName, resultSet.getInt(index) > 0); - } - - @Override - public void setCursorField(final PreparedStatement preparedStatement, - final int parameterIndex, - final MysqlType cursorFieldType, - final String value) - throws SQLException { - switch (cursorFieldType) { - case BIT -> setBit(preparedStatement, parameterIndex, value); - case BOOLEAN -> setBoolean(preparedStatement, parameterIndex, value); - case YEAR, TINYINT, TINYINT_UNSIGNED, SMALLINT, SMALLINT_UNSIGNED, MEDIUMINT, MEDIUMINT_UNSIGNED -> setInteger(preparedStatement, - parameterIndex, - value); - case INT, INT_UNSIGNED, BIGINT, BIGINT_UNSIGNED -> setBigInteger(preparedStatement, parameterIndex, value); - case FLOAT, FLOAT_UNSIGNED, DOUBLE, DOUBLE_UNSIGNED -> setDouble(preparedStatement, parameterIndex, value); - case DECIMAL, DECIMAL_UNSIGNED -> setDecimal(preparedStatement, parameterIndex, value); - case DATE -> setDate(preparedStatement, parameterIndex, value); - case DATETIME -> setTimestamp(preparedStatement, parameterIndex, value); - case TIMESTAMP -> setTimestampWithTimezone(preparedStatement, parameterIndex, value); - case TIME -> setTime(preparedStatement, parameterIndex, value); - case CHAR, VARCHAR, TINYTEXT, TEXT, MEDIUMTEXT, LONGTEXT, ENUM, SET -> setString(preparedStatement, parameterIndex, value); - case TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, BINARY, VARBINARY -> setBinary(preparedStatement, parameterIndex, value); - // since cursor are expected to be comparable, handle cursor typing strictly and error on - // unrecognized types - default -> throw new IllegalArgumentException(String.format("%s cannot be used as a cursor.", cursorFieldType)); - } - } - - @Override - public MysqlType getDatabaseFieldType(final JsonNode field) { - try { - // MysqlType#getByName can handle the full MySQL type name - // e.g. MEDIUMINT UNSIGNED - final MysqlType literalType = MysqlType.getByName(field.get(INTERNAL_COLUMN_TYPE_NAME).asText()); - final int columnSize = field.get(INTERNAL_COLUMN_SIZE).asInt(); - switch (literalType) { - // BIT(1) and TINYINT(1) are interpreted as boolean - case BIT, TINYINT -> { - if (columnSize == 1) { - return MysqlType.BOOLEAN; - } - } - case YEAR -> { - return SMALLINT; - } - // When CHAR[N] and VARCHAR[N] columns have binary character set, the returned - // types are BINARY[N] and VARBINARY[N], respectively. So we don't need to - // convert them here. This is verified in MySqlSourceDatatypeTest. - case DECIMAL -> { - if (field.get(INTERNAL_DECIMAL_DIGITS) != null && field.get(INTERNAL_DECIMAL_DIGITS).asInt() == 0) { - return BIGINT; - } - } - case DECIMAL_UNSIGNED -> { - if (field.get(INTERNAL_DECIMAL_DIGITS) != null && field.get(INTERNAL_DECIMAL_DIGITS).asInt() == 0) { - return BIGINT_UNSIGNED; - } - } - } - return literalType; - } catch (final IllegalArgumentException ex) { - LOGGER.warn(String.format("Could not convert column: %s from table: %s.%s with type: %s (type name: %s). Casting to VARCHAR.", - field.get(INTERNAL_COLUMN_NAME), - field.get(INTERNAL_SCHEMA_NAME), - field.get(INTERNAL_TABLE_NAME), - field.get(INTERNAL_COLUMN_TYPE), - field.get(INTERNAL_COLUMN_TYPE_NAME))); - return MysqlType.VARCHAR; - } - } - - @Override - public boolean isCursorType(final MysqlType type) { - return ALLOWED_CURSOR_TYPES.contains(type); - } - - @Override - public JsonSchemaType getAirbyteType(final MysqlType mysqlType) { - return switch (mysqlType) { - case - // TINYINT(1) is boolean, but it should have been converted to MysqlType.BOOLEAN in {@link - // getFieldType} - TINYINT, TINYINT_UNSIGNED, SMALLINT, SMALLINT_UNSIGNED, INT, MEDIUMINT, MEDIUMINT_UNSIGNED, INT_UNSIGNED, BIGINT, BIGINT_UNSIGNED -> JsonSchemaType.INTEGER; - case FLOAT, FLOAT_UNSIGNED, DOUBLE, DOUBLE_UNSIGNED, DECIMAL, DECIMAL_UNSIGNED -> JsonSchemaType.NUMBER; - case BOOLEAN -> JsonSchemaType.BOOLEAN; - case NULL -> JsonSchemaType.NULL; - // BIT(1) is boolean, but it should have been converted to MysqlType.BOOLEAN in {@link getFieldType} - case BIT, TINYBLOB, BLOB, MEDIUMBLOB, LONGBLOB, BINARY, VARBINARY, GEOMETRY -> JsonSchemaType.STRING_BASE_64; - case TIME -> JsonSchemaType.STRING_TIME_WITHOUT_TIMEZONE; - case DATETIME -> JsonSchemaType.STRING_TIMESTAMP_WITHOUT_TIMEZONE; - case TIMESTAMP -> JsonSchemaType.STRING_TIMESTAMP_WITH_TIMEZONE; - case DATE -> JsonSchemaType.STRING_DATE; - default -> JsonSchemaType.STRING; - }; - } - - @Override - protected void setDate(final PreparedStatement preparedStatement, final int parameterIndex, final String value) throws SQLException { - try { - preparedStatement.setObject(parameterIndex, LocalDate.parse(value)); - } catch (final DateTimeParseException e) { - // This is just for backward compatibility for connectors created on versions before PR - // https://github.com/airbytehq/airbyte/pull/15504 - LOGGER.warn("Exception occurred while trying to parse value for date column the new way, trying the old way", e); - super.setDate(preparedStatement, parameterIndex, value); - } - } - - @Override - protected void setTimestamp(final PreparedStatement preparedStatement, final int parameterIndex, final String value) throws SQLException { - try { - preparedStatement.setObject(parameterIndex, LocalDateTime.parse(value)); - } catch (final DateTimeParseException e) { - // This is just for backward compatibility for connectors created on versions before PR - // https://github.com/airbytehq/airbyte/pull/15504 - LOGGER.warn("Exception occurred while trying to parse value for datetime column the new way, trying the old way", e); - preparedStatement.setObject(parameterIndex, OffsetDateTime.parse(value)); - } - } - - private void setTimestampWithTimezone(final PreparedStatement preparedStatement, final int parameterIndex, final String value) throws SQLException { - try { - preparedStatement.setObject(parameterIndex, OffsetDateTime.parse(value)); - } catch (final DateTimeParseException e) { - // This is just for backward compatibility for connectors created on versions before PR - // https://github.com/airbytehq/airbyte/pull/15504 - LOGGER.warn("Exception occurred while trying to parse value for timestamp column the new way, trying the old way", e); - preparedStatement.setObject(parameterIndex, LocalDateTime.parse(value)); - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSpecConstants.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSpecConstants.java deleted file mode 100644 index 7735470482da..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlSpecConstants.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -// Constants defined in -// airbyte-integrations/connectors/source-postgres/src/main/resources/spec.json. -public class MySqlSpecConstants { - - public static final String INVALID_CDC_CURSOR_POSITION_PROPERTY = "invalid_cdc_cursor_position_behavior"; - public static final String FAIL_SYNC_OPTION = "Fail sync"; - public static final String RESYNC_DATA_OPTION = "Re-sync data"; - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlStreamingQueryConfig.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlStreamingQueryConfig.java deleted file mode 100644 index 029cfc2c3d66..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/MySqlStreamingQueryConfig.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import io.airbyte.cdk.db.jdbc.streaming.AdaptiveStreamingQueryConfig; -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlStreamingQueryConfig extends AdaptiveStreamingQueryConfig { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlStreamingQueryConfig.class); - - public MySqlStreamingQueryConfig() { - super(); - } - - @Override - public void initialize(final Connection connection, final Statement preparedStatement) throws SQLException { - preparedStatement.setFetchSize(Integer.MIN_VALUE); - LOGGER.info("Set initial fetch size: {} rows", preparedStatement.getFetchSize()); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CdcConfigurationHelper.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CdcConfigurationHelper.java deleted file mode 100644 index 2f5d02caf72f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CdcConfigurationHelper.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.commons.functional.CheckedConsumer; -import java.sql.SQLException; -import java.time.ZoneId; -import java.util.List; -import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper class for MySqlSource used to check cdc configuration in case of: - *

- * 1. adding new source and checking operations #getCheckOperations method. - *

- *

- * 2. checking whether binlog required from saved cdc offset is available on mysql server - * #checkBinlog method - *

- * 3. configuring initial CDC wait time. TODO : There is a lot of shared logic for this - * functionality between MySQL and Postgres. Refactor it to reduce code de-duplication. - */ -public class CdcConfigurationHelper { - - private static final Logger LOGGER = LoggerFactory.getLogger(CdcConfigurationHelper.class); - private static final String LOG_BIN = "log_bin"; - private static final String BINLOG_FORMAT = "binlog_format"; - private static final String BINLOG_ROW_IMAGE = "binlog_row_image"; - - /** - * Method will get required configurations for cdc sync - * - * @return list of List> - */ - public static List> getCheckOperations() { - return List.of(getMasterStatusOperation(), - getCheckOperation(LOG_BIN, "ON"), - getCheckOperation(BINLOG_FORMAT, "ROW"), - getCheckOperation(BINLOG_ROW_IMAGE, "FULL")); - - } - - // Checks whether the user has REPLICATION CLIENT privilege needed to query status information about - // the binary log files, which are needed for CDC. - private static CheckedConsumer getMasterStatusOperation() { - return database -> { - try { - database.unsafeResultSetQuery( - connection -> connection.createStatement().executeQuery("SHOW MASTER STATUS"), - resultSet -> resultSet); - } catch (final SQLException e) { - throw new ConfigErrorException("Please grant REPLICATION CLIENT privilege, so that binary log files are available" - + " for CDC mode."); - } - }; - } - - private static CheckedConsumer getCheckOperation(final String name, final String value) { - return database -> { - final List result = database.queryStrings( - connection -> connection.createStatement().executeQuery(String.format("show variables where Variable_name = '%s'", name)), - resultSet -> resultSet.getString("Value")); - - if (result.size() != 1) { - throw new RuntimeException("Could not query the variable " + name); - } - - final String resultValue = result.get(0); - if (!resultValue.equalsIgnoreCase(value)) { - throw new RuntimeException(String.format("The variable \"%s\" should be set to \"%s\", but it is \"%s\"", name, value, resultValue)); - } - }; - } - - private static Optional getCdcServerTimezone(final JsonNode config) { - final JsonNode replicationMethod = config.get("replication_method"); - if (replicationMethod != null && replicationMethod.has("server_time_zone")) { - final String serverTimeZone = config.get("replication_method").get("server_time_zone").asText(); - return Optional.of(serverTimeZone); - } - return Optional.empty(); - } - - public static void checkServerTimeZoneConfig(final JsonNode config) { - final Optional serverTimeZone = getCdcServerTimezone(config); - if (serverTimeZone.isPresent()) { - final String timeZone = serverTimeZone.get(); - if (!timeZone.isEmpty() && !ZoneId.getAvailableZoneIds().contains((timeZone))) { - throw new IllegalArgumentException(String.format("Given timezone %s is not valid. The given timezone must conform to the IANNA standard. " - + "See https://www.iana.org/time-zones for more details", serverTimeZone.get())); - } - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CustomMySQLTinyIntOneToBooleanConverter.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CustomMySQLTinyIntOneToBooleanConverter.java deleted file mode 100644 index 38a4162287b7..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/CustomMySQLTinyIntOneToBooleanConverter.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import io.debezium.connector.binlog.converters.TinyIntOneToBooleanConverter; -import io.debezium.spi.converter.RelationalColumn; -import org.apache.kafka.connect.data.SchemaBuilder; - -public class CustomMySQLTinyIntOneToBooleanConverter extends TinyIntOneToBooleanConverter { - - @Override - public void converterFor(final RelationalColumn field, final ConverterRegistration registration) { - if (!"TINYINT".equalsIgnoreCase(field.typeName())) { - return; - } - super.converterFor(field, registration); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySQLDateTimeConverter.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySQLDateTimeConverter.java deleted file mode 100644 index 233948c5b31b..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySQLDateTimeConverter.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import io.airbyte.cdk.db.jdbc.DateTimeConverter; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.integrations.debezium.internals.DebeziumConverterUtils; -import io.debezium.spi.converter.CustomConverter; -import io.debezium.spi.converter.RelationalColumn; -import io.debezium.time.Conversions; -import java.time.LocalDate; -import java.time.LocalTime; -import java.util.Arrays; -import java.util.Locale; -import java.util.Properties; -import java.util.concurrent.TimeUnit; -import org.apache.kafka.connect.data.SchemaBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This is a custom debezium converter used in MySQL to handle the DATETIME data type. We need a - * custom converter cause by default debezium returns the DATETIME values as numbers. We need to - * convert it to proper format. Ref : - * https://debezium.io/documentation/reference/2.1/development/converters.html This is built from - * reference with {@link io.debezium.connector.mysql.converters.TinyIntOneToBooleanConverter} If you - * rename this class then remember to rename the datetime.type property value in - * {@link MySqlCdcProperties#commonProperties(JdbcDatabase)} (If you don't rename, a test would - * still fail but it might be tricky to figure out where to change the property name) - */ -public class MySQLDateTimeConverter implements CustomConverter { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySQLDateTimeConverter.class); - - private final String[] DATE_TYPES = {"DATE", "DATETIME", "TIME", "TIMESTAMP"}; - - @Override - public void configure(final Properties props) {} - - @Override - public void converterFor(final RelationalColumn field, final ConverterRegistration registration) { - if (Arrays.stream(DATE_TYPES).anyMatch(s -> s.equalsIgnoreCase(field.typeName()))) { - registerDate(field, registration); - } - } - - private int getTimePrecision(final RelationalColumn field) { - return field.length().orElse(-1); - } - - // Ref : - // https://debezium.io/documentation/reference/2.1/connectors/mysql.html#mysql-temporal-types - private void registerDate(final RelationalColumn field, final ConverterRegistration registration) { - final var fieldType = field.typeName(); - - registration.register(SchemaBuilder.string().optional(), x -> { - if (x == null) { - return DebeziumConverterUtils.convertDefaultValue(field); - } - - switch (fieldType.toUpperCase(Locale.ROOT)) { - case "DATETIME": - if (x instanceof final Long l) { - if (getTimePrecision(field) <= 3) { - return DateTimeConverter.convertToTimestamp(Conversions.toInstantFromMillis(l)); - } - if (getTimePrecision(field) <= 6) { - return DateTimeConverter.convertToTimestamp(Conversions.toInstantFromMicros(l)); - } - } - return DateTimeConverter.convertToTimestamp(x); - case "DATE": - if (x instanceof final Integer i) { - return DateTimeConverter.convertToDate(LocalDate.ofEpochDay(i)); - } - return DateTimeConverter.convertToDate(x); - case "TIME": - if (x instanceof Long) { - long l = Math.multiplyExact((Long) x, TimeUnit.MICROSECONDS.toNanos(1)); - return DateTimeConverter.convertToTime(LocalTime.ofNanoOfDay(l)); - } - return DateTimeConverter.convertToTime(x); - case "TIMESTAMP": - return DateTimeConverter.convertToTimestampWithTimezone(x); - default: - throw new IllegalArgumentException("Unknown field type " + fieldType.toUpperCase(Locale.ROOT)); - } - }); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcConnectorMetadataInjector.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcConnectorMetadataInjector.java deleted file mode 100644 index d43f83e725c9..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcConnectorMetadataInjector.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_DELETED_AT; -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_UPDATED_AT; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_DEFAULT_CURSOR; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_LOG_FILE; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_LOG_POS; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.airbyte.cdk.integrations.debezium.CdcMetadataInjector; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil.MysqlDebeziumStateAttributes; -import java.time.Instant; -import java.util.concurrent.atomic.AtomicLong; - -public class MySqlCdcConnectorMetadataInjector implements CdcMetadataInjector { - - private final long emittedAtConverted; - - // This now makes this class stateful. Please make sure to use the same instance within a sync - private final AtomicLong recordCounter = new AtomicLong(1); - private static final long ONE_HUNDRED_MILLION = 100_000_000; - private static MySqlCdcConnectorMetadataInjector mySqlCdcConnectorMetadataInjector; - - private MySqlCdcConnectorMetadataInjector(final Instant emittedAt) { - this.emittedAtConverted = emittedAt.getEpochSecond() * ONE_HUNDRED_MILLION; - } - - public static MySqlCdcConnectorMetadataInjector getInstance(final Instant emittedAt) { - if (mySqlCdcConnectorMetadataInjector == null) { - mySqlCdcConnectorMetadataInjector = new MySqlCdcConnectorMetadataInjector(emittedAt); - } - - return mySqlCdcConnectorMetadataInjector; - } - - @Override - public void addMetaData(final ObjectNode event, final JsonNode source) { - event.put(CDC_LOG_FILE, source.get("file").asText()); - event.put(CDC_LOG_POS, source.get("pos").asLong()); - event.put(CDC_DEFAULT_CURSOR, getCdcDefaultCursor()); - } - - @Override - public void addMetaDataToRowsFetchedOutsideDebezium(final ObjectNode record, - final String transactionTimestamp, - final MysqlDebeziumStateAttributes debeziumStateAttributes) { - record.put(CDC_UPDATED_AT, transactionTimestamp); - record.put(CDC_LOG_FILE, debeziumStateAttributes.binlogFilename()); - record.put(CDC_LOG_POS, debeziumStateAttributes.binlogPosition()); - record.put(CDC_DELETED_AT, (String) null); - record.put(CDC_DEFAULT_CURSOR, getCdcDefaultCursor()); - } - - @Override - public String namespace(final JsonNode source) { - return source.get("db").asText(); - } - - @Override - public String name(JsonNode source) { - return source.get("table").asText(); - } - - private Long getCdcDefaultCursor() { - return this.emittedAtConverted + this.recordCounter.getAndIncrement(); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcPosition.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcPosition.java deleted file mode 100644 index 04cc8430142f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcPosition.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import java.util.Objects; - -public class MySqlCdcPosition { - - public final String fileName; - public final Long position; - - public MySqlCdcPosition(final String fileName, final Long position) { - this.fileName = fileName; - this.position = position; - } - - @Override - public boolean equals(final Object obj) { - if (obj instanceof final MySqlCdcPosition mySqlCdcPosition) { - return fileName.equals(mySqlCdcPosition.fileName) && mySqlCdcPosition.position.equals(position); - } - return false; - } - - @Override - public int hashCode() { - return Objects.hash(fileName, position); - } - - @Override - public String toString() { - return "FileName: " + fileName + ", Position : " + position; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcProperties.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcProperties.java deleted file mode 100644 index 9b54d0588f67..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcProperties.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import static io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.CLIENT_KEY_STORE_PASS; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.CLIENT_KEY_STORE_URL; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.SSL_MODE; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.TRUST_KEY_STORE_PASS; -import static io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.TRUST_KEY_STORE_URL; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.source.jdbc.JdbcSSLConnectionUtils.SslMode; -import io.airbyte.integrations.source.mysql.MySqlSource; -import java.net.URI; -import java.nio.file.Path; -import java.time.Duration; -import java.util.Properties; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlCdcProperties { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlCdcProperties.class); - private static final Duration HEARTBEAT_INTERVAL = Duration.ofSeconds(10L); - - // Test execution latency is lower when heartbeats are more frequent. - private static final Duration HEARTBEAT_INTERVAL_IN_TESTS = Duration.ofSeconds(1L); - - public static Properties getDebeziumProperties(final JdbcDatabase database) { - final JsonNode sourceConfig = database.getSourceConfig(); - final Properties props = commonProperties(database); - // snapshot config - if (sourceConfig.has("snapshot_mode")) { - // The parameter `snapshot_mode` is passed in test to simulate reading the binlog directly and skip - // initial snapshot - props.setProperty("snapshot.mode", sourceConfig.get("snapshot_mode").asText()); - } else { - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-snapshot-mode - props.setProperty("snapshot.mode", "when_needed"); - } - - return props; - } - - private static Properties commonProperties(final JdbcDatabase database) { - final Properties props = new Properties(); - final JsonNode sourceConfig = database.getSourceConfig(); - final JsonNode dbConfig = database.getDatabaseConfig(); - // debezium engine configuration - props.setProperty("connector.class", "io.debezium.connector.mysql.MySqlConnector"); - - props.setProperty("database.server.id", String.valueOf(generateServerID())); - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-boolean-values - // https://debezium.io/documentation/reference/2.2/development/converters.html - /** - * {@link io.debezium.connector.mysql.converters.TinyIntOneToBooleanConverter} - * {@link MySQLConverter} - */ - props.setProperty("converters", "boolean, datetime"); - props.setProperty("boolean.type", CustomMySQLTinyIntOneToBooleanConverter.class.getName()); - props.setProperty("datetime.type", MySQLDateTimeConverter.class.getName()); - - final Duration heartbeatInterval = - (database.getSourceConfig().has("is_test") && database.getSourceConfig().get("is_test").asBoolean()) - ? HEARTBEAT_INTERVAL_IN_TESTS - : HEARTBEAT_INTERVAL; - props.setProperty("heartbeat.interval.ms", Long.toString(heartbeatInterval.toMillis())); - - // For CDC mode, the user cannot provide timezone arguments as JDBC parameters - they are - // specifically defined in the replication_method - // config. - if (sourceConfig.get("replication_method").has("server_time_zone")) { - final String serverTimeZone = sourceConfig.get("replication_method").get("server_time_zone").asText(); - if (!serverTimeZone.isEmpty()) { - /** - * Per Debezium docs, - * https://debezium.io/documentation/reference/stable/connectors/mysql.html#mysql-temporal-types - * this property is now connectionTimeZone {@link com.mysql.cj.conf.PropertyKey#connectionTimeZone} - **/ - props.setProperty("database.connectionTimeZone", serverTimeZone); - } - } - - // Check params for SSL connection in config and add properties for CDC SSL connection - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-database-ssl-mode - if (!sourceConfig.has(JdbcUtils.SSL_KEY) || sourceConfig.get(JdbcUtils.SSL_KEY).asBoolean()) { - if (dbConfig.has(SSL_MODE) && !dbConfig.get(SSL_MODE).asText().isEmpty()) { - props.setProperty("database.ssl.mode", MySqlSource.toSslJdbcParam(SslMode.valueOf(dbConfig.get(SSL_MODE).asText()))); - - if (dbConfig.has(TRUST_KEY_STORE_URL) && !dbConfig.get(TRUST_KEY_STORE_URL).asText().isEmpty()) { - props.setProperty("database.ssl.truststore", Path.of(URI.create(dbConfig.get(TRUST_KEY_STORE_URL).asText())).toString()); - } - - if (dbConfig.has(TRUST_KEY_STORE_PASS) && !dbConfig.get(TRUST_KEY_STORE_PASS).asText().isEmpty()) { - props.setProperty("database.ssl.truststore.password", dbConfig.get(TRUST_KEY_STORE_PASS).asText()); - } - - if (dbConfig.has(CLIENT_KEY_STORE_URL) && !dbConfig.get(CLIENT_KEY_STORE_URL).asText().isEmpty()) { - props.setProperty("database.ssl.keystore", Path.of(URI.create(dbConfig.get(CLIENT_KEY_STORE_URL).asText())).toString()); - } - - if (dbConfig.has(CLIENT_KEY_STORE_PASS) && !dbConfig.get(CLIENT_KEY_STORE_PASS).asText().isEmpty()) { - props.setProperty("database.ssl.keystore.password", dbConfig.get(CLIENT_KEY_STORE_PASS).asText()); - } - - } else { - props.setProperty("database.ssl.mode", "required"); - } - } - - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-snapshot-locking-mode - // This is to make sure other database clients are allowed to write to a table while Airbyte is - // taking a snapshot. There is a risk involved that - // if any database client makes a schema change then the sync might break - props.setProperty("snapshot.locking.mode", "none"); - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-include-schema-changes - props.setProperty("include.schema.changes", "false"); - // This to make sure that binary data represented as a base64-encoded String. - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-binary-handling-mode - props.setProperty("binary.handling.mode", "base64"); - props.setProperty("database.include.list", sourceConfig.get("database").asText()); - - return props; - } - - private static int generateServerID() { - final int min = 5400; - final int max = 6400; - - final int serverId = (int) Math.floor(Math.random() * (max - min + 1) + min); - LOGGER.info("Randomly generated Server ID : " + serverId); - return serverId; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcSavedInfoFetcher.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcSavedInfoFetcher.java deleted file mode 100644 index 7d23671afacb..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcSavedInfoFetcher.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.IS_COMPRESSED; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.MYSQL_CDC_OFFSET; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.MYSQL_DB_HISTORY; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.integrations.debezium.CdcSavedInfoFetcher; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteSchemaHistoryStorage.SchemaHistory; -import io.airbyte.cdk.integrations.source.relationaldb.models.CdcState; -import java.util.Optional; - -public class MySqlCdcSavedInfoFetcher implements CdcSavedInfoFetcher { - - private final JsonNode savedOffset; - private final JsonNode savedSchemaHistory; - private final boolean isSavedSchemaHistoryCompressed; - - public MySqlCdcSavedInfoFetcher(final CdcState savedState) { - final boolean savedStatePresent = savedState != null && savedState.getState() != null; - this.savedOffset = savedStatePresent ? savedState.getState().get(MYSQL_CDC_OFFSET) : null; - this.savedSchemaHistory = savedStatePresent ? savedState.getState().get(MYSQL_DB_HISTORY) : null; - this.isSavedSchemaHistoryCompressed = - savedStatePresent && savedState.getState().has(IS_COMPRESSED) && savedState.getState().get(IS_COMPRESSED).asBoolean(); - } - - @Override - public JsonNode getSavedOffset() { - return savedOffset; - } - - @Override - public SchemaHistory> getSavedSchemaHistory() { - return new SchemaHistory<>(Optional.ofNullable(savedSchemaHistory), isSavedSchemaHistoryCompressed); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcStateHandler.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcStateHandler.java deleted file mode 100644 index a6f672c7ab40..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcStateHandler.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import static io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil.serialize; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.COMPRESSION_ENABLED; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.integrations.debezium.CdcStateHandler; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteSchemaHistoryStorage.SchemaHistory; -import io.airbyte.cdk.integrations.source.relationaldb.models.CdcState; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManager; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.AirbyteMessage.Type; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import java.util.Map; -import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlCdcStateHandler implements CdcStateHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlCdcStateHandler.class); - - private final StateManager stateManager; - - public MySqlCdcStateHandler(final StateManager stateManager) { - this.stateManager = stateManager; - } - - @Override - public boolean isCdcCheckpointEnabled() { - return true; - } - - @Override - public AirbyteMessage saveState(final Map offset, final SchemaHistory dbHistory) { - final JsonNode asJson = serialize(offset, dbHistory); - - LOGGER.info("debezium state: {}", asJson); - - final CdcState cdcState = new CdcState().withState(asJson); - stateManager.getCdcStateManager().setCdcState(cdcState); - /* - * Namespace pair is ignored by global state manager, but is needed for satisfy the API contract. - * Therefore, provide an empty optional. - */ - final AirbyteStateMessage stateMessage = stateManager.emit(Optional.empty()); - return new AirbyteMessage().withType(Type.STATE).withState(stateMessage); - } - - @Override - public AirbyteMessage saveStateAfterCompletionOfSnapshotOfNewStreams() { - LOGGER.info("Snapshot of new tables is complete, saving state"); - /* - * Namespace pair is ignored by global state manager, but is needed for satisfy the API contract. - * Therefore, provide an empty optional. - */ - final AirbyteStateMessage stateMessage = stateManager.emit(Optional.empty()); - return new AirbyteMessage().withType(Type.STATE).withState(stateMessage); - } - - @Override - public boolean compressSchemaHistoryForState() { - return COMPRESSION_ENABLED; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcTargetPosition.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcTargetPosition.java deleted file mode 100644 index 201a2417b3ef..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlCdcTargetPosition.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.integrations.debezium.CdcTargetPosition; -import io.airbyte.cdk.integrations.debezium.internals.ChangeEventWithMetadata; -import io.airbyte.cdk.integrations.debezium.internals.SnapshotMetadata; -import io.airbyte.commons.json.Jsons; -import java.sql.SQLException; -import java.util.List; -import java.util.Map; -import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlCdcTargetPosition implements CdcTargetPosition { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlCdcTargetPosition.class); - private final MySqlCdcPosition targetPosition; - - public MySqlCdcTargetPosition(final String fileName, final Long position) { - this(new MySqlCdcPosition(fileName, position)); - } - - @Override - public boolean equals(final Object obj) { - if (obj instanceof final MySqlCdcTargetPosition cdcTargetPosition) { - return targetPosition.equals(cdcTargetPosition.targetPosition); - } - return false; - } - - @Override - public int hashCode() { - return targetPosition.hashCode(); - } - - @Override - public String toString() { - return targetPosition.toString(); - } - - public MySqlCdcTargetPosition(final MySqlCdcPosition targetPosition) { - this.targetPosition = targetPosition; - } - - public static MySqlCdcTargetPosition targetPosition(final JdbcDatabase database) { - try (final Stream stream = database.unsafeResultSetQuery( - connection -> connection.createStatement().executeQuery("SHOW MASTER STATUS"), - resultSet -> { - final String file = resultSet.getString("File"); - final long position = resultSet.getLong("Position"); - if (file == null || position == 0) { - return new MySqlCdcTargetPosition(null, null); - } - return new MySqlCdcTargetPosition(file, position); - })) { - final List masterStatus = stream.toList(); - final MySqlCdcTargetPosition targetPosition = masterStatus.get(0); - LOGGER.info("Target File position : " + targetPosition); - return targetPosition; - } catch (final SQLException e) { - throw new RuntimeException(e); - } - - } - - @Override - public boolean reachedTargetPosition(final ChangeEventWithMetadata changeEventWithMetadata) { - if (changeEventWithMetadata.isSnapshotEvent()) { - return false; - } else if (SnapshotMetadata.LAST == changeEventWithMetadata.getSnapshotMetadata()) { - LOGGER.info("Signalling close because Snapshot is complete"); - return true; - } else { - final String eventFileName = changeEventWithMetadata.getEventValueAsJson().get("source").get("file").asText(); - final long eventPosition = changeEventWithMetadata.getEventValueAsJson().get("source").get("pos").asLong(); - final boolean isEventPositionAfter = - eventFileName.compareTo(targetPosition.fileName) > 0 || (eventFileName.compareTo( - targetPosition.fileName) == 0 && eventPosition >= targetPosition.position); - if (isEventPositionAfter) { - LOGGER.info("Signalling close because record's binlog file : " + eventFileName + " , position : " + eventPosition - + " is after target file : " - + targetPosition.fileName + " , target position : " + targetPosition.position); - } - return isEventPositionAfter; - } - - } - - @Override - public boolean reachedTargetPosition(final MySqlCdcPosition positionFromHeartbeat) { - return positionFromHeartbeat.fileName.compareTo(targetPosition.fileName) > 0 || - (positionFromHeartbeat.fileName.compareTo(targetPosition.fileName) == 0 - && positionFromHeartbeat.position >= targetPosition.position); - } - - @Override - public boolean isHeartbeatSupported() { - return true; - } - - @Override - public boolean isEventAheadOffset(final Map offset, final ChangeEventWithMetadata event) { - if (offset.size() != 1) { - return false; - } - - final String eventFileName = event.getEventValueAsJson().get("source").get("file").asText(); - final long eventPosition = event.getEventValueAsJson().get("source").get("pos").asLong(); - - final JsonNode offsetJson = Jsons.deserialize((String) offset.values().toArray()[0]); - - final String offsetFileName = offsetJson.get("file").asText(); - final long offsetPosition = offsetJson.get("pos").asLong(); - if (eventFileName.compareTo(offsetFileName) != 0) { - return eventFileName.compareTo(offsetFileName) > 0; - } - - return eventPosition > offsetPosition; - } - - @Override - public boolean isSameOffset(final Map offsetA, final Map offsetB) { - if ((offsetA == null || offsetA.size() != 1) || (offsetB == null || offsetB.size() != 1)) { - return false; - } - - final JsonNode offsetJsonA = Jsons.deserialize((String) offsetA.values().toArray()[0]); - final String offsetAFileName = offsetJsonA.get("file").asText(); - final long offsetAPosition = offsetJsonA.get("pos").asLong(); - - final JsonNode offsetJsonB = Jsons.deserialize((String) offsetB.values().toArray()[0]); - final String offsetBFileName = offsetJsonB.get("file").asText(); - final long offsetBPosition = offsetJsonB.get("pos").asLong(); - - return offsetAFileName.equals(offsetBFileName) && offsetAPosition == offsetBPosition; - } - - @Override - public MySqlCdcPosition extractPositionFromHeartbeatOffset(final Map sourceOffset) { - return new MySqlCdcPosition(sourceOffset.get("file").toString(), (Long) sourceOffset.get("pos")); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumStateUtil.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumStateUtil.java deleted file mode 100644 index 36d6115a3cec..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumStateUtil.java +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.COMPRESSION_ENABLED; -import static io.debezium.relational.RelationalDatabaseConnectorConfig.DATABASE_NAME; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.annotations.VisibleForTesting; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteFileOffsetBackingStore; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteSchemaHistoryStorage; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteSchemaHistoryStorage.SchemaHistory; -import io.airbyte.cdk.integrations.debezium.internals.DebeziumPropertiesManager; -import io.airbyte.cdk.integrations.debezium.internals.DebeziumRecordPublisher; -import io.airbyte.cdk.integrations.debezium.internals.DebeziumStateUtil; -import io.airbyte.cdk.integrations.debezium.internals.RecordWaitTimeUtil; -import io.airbyte.cdk.integrations.debezium.internals.RelationalDbDebeziumPropertiesManager; -import io.airbyte.commons.json.Jsons; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.debezium.config.Configuration; -import io.debezium.connector.common.OffsetReader; -import io.debezium.connector.mysql.MySqlConnectorConfig; -import io.debezium.connector.mysql.MySqlOffsetContext; -import io.debezium.connector.mysql.MySqlOffsetContext.Loader; -import io.debezium.connector.mysql.MySqlPartition; -import io.debezium.connector.mysql.gtid.MySqlGtidSet; -import io.debezium.engine.ChangeEvent; -import io.debezium.pipeline.spi.Offsets; -import io.debezium.pipeline.spi.Partition; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; -import org.apache.kafka.connect.storage.FileOffsetBackingStore; -import org.apache.kafka.connect.storage.OffsetStorageReaderImpl; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlDebeziumStateUtil implements DebeziumStateUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlDebeziumStateUtil.class); - - public boolean savedOffsetStillPresentOnServer(final JdbcDatabase database, final MysqlDebeziumStateAttributes savedState) { - if (savedState.gtidSet().isPresent()) { - final Optional availableGtidStr = getStateAttributesFromDB(database).gtidSet(); - if (availableGtidStr.isEmpty()) { - // Last offsets had GTIDs but the server does not use them - LOGGER.info("Connector used GTIDs previously, but MySQL server does not know of any GTIDs or they are not enabled"); - return false; - } - final MySqlGtidSet gtidSetFromSavedState = new MySqlGtidSet(savedState.gtidSet().get()); - // Get the GTID set that is available in the server - final MySqlGtidSet availableGtidSet = new MySqlGtidSet(availableGtidStr.get()); - if (gtidSetFromSavedState.isContainedWithin(availableGtidSet)) { - LOGGER.info("MySQL server current GTID set {} does contain the GTID set required by the connector {}", availableGtidSet, - gtidSetFromSavedState); - final Optional gtidSetToReplicate = subtractGtidSet(availableGtidSet, gtidSetFromSavedState, database); - if (gtidSetToReplicate.isPresent()) { - final Optional purgedGtidSet = purgedGtidSet(database); - if (purgedGtidSet.isPresent()) { - LOGGER.info("MySQL server has already purged {} GTIDs", purgedGtidSet.get()); - final Optional nonPurgedGtidSetToReplicate = subtractGtidSet(gtidSetToReplicate.get(), purgedGtidSet.get(), database); - if (nonPurgedGtidSetToReplicate.isPresent()) { - LOGGER.info("GTIDs known by the MySQL server but not processed yet {}, for replication are available only {}", gtidSetToReplicate, - nonPurgedGtidSetToReplicate); - if (!gtidSetToReplicate.equals(nonPurgedGtidSetToReplicate)) { - LOGGER.info("Some of the GTIDs needed to replicate have been already purged by MySQL server"); - return false; - } - } - } - } - return true; - } - LOGGER.info("Connector last known GTIDs are {}, but MySQL server only has {}", gtidSetFromSavedState, availableGtidSet); - return false; - } - - final List existingLogFiles = getExistingLogFiles(database); - final boolean found = existingLogFiles.stream().anyMatch(savedState.binlogFilename()::equals); - if (!found) { - LOGGER.info("Connector requires binlog file '{}', but MySQL server only has {}", savedState.binlogFilename(), - String.join(", ", existingLogFiles)); - } else { - LOGGER.info("MySQL server has the binlog file '{}' required by the connector", savedState.binlogFilename()); - } - - return found; - - } - - private List getExistingLogFiles(final JdbcDatabase database) { - try (final Stream stream = database.unsafeResultSetQuery( - connection -> connection.createStatement().executeQuery("SHOW BINARY LOGS"), - resultSet -> resultSet.getString(1))) { - return stream.toList(); - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - private Optional subtractGtidSet(final MySqlGtidSet set1, final MySqlGtidSet set2, final JdbcDatabase database) { - try (final Stream stream = database.unsafeResultSetQuery( - connection -> { - final PreparedStatement ps = connection.prepareStatement("SELECT GTID_SUBTRACT(?, ?)"); - ps.setString(1, set1.toString()); - ps.setString(2, set2.toString()); - return ps.executeQuery(); - }, - resultSet -> new MySqlGtidSet(resultSet.getString(1)))) { - final List gtidSets = stream.toList(); - if (gtidSets.isEmpty()) { - return Optional.empty(); - } else if (gtidSets.size() == 1) { - return Optional.of(gtidSets.get(0)); - } else { - throw new RuntimeException("Not expecting gtid set size to be greater than 1"); - } - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - private Optional purgedGtidSet(final JdbcDatabase database) { - try (final Stream> stream = database.unsafeResultSetQuery( - connection -> connection.createStatement().executeQuery("SELECT @@global.gtid_purged"), - resultSet -> { - if (resultSet.getMetaData().getColumnCount() > 0) { - String string = resultSet.getString(1); - if (string != null && !string.isEmpty()) { - return Optional.of(new MySqlGtidSet(string)); - } - } - return Optional.empty(); - })) { - final List> gtidSet = stream.toList(); - if (gtidSet.isEmpty()) { - return Optional.empty(); - } else if (gtidSet.size() == 1) { - return gtidSet.get(0); - } else { - throw new RuntimeException("Not expecting the size to be greater than 1"); - } - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - public Optional savedOffset(final Properties baseProperties, - final ConfiguredAirbyteCatalog catalog, - final JsonNode cdcOffset, - final JsonNode config) { - if (Objects.isNull(cdcOffset)) { - return Optional.empty(); - } - - final var offsetManager = AirbyteFileOffsetBackingStore.initializeState(cdcOffset, Optional.empty()); - final DebeziumPropertiesManager debeziumPropertiesManager = new RelationalDbDebeziumPropertiesManager(baseProperties, config, catalog, - new ArrayList()); - final Properties debeziumProperties = debeziumPropertiesManager.getDebeziumProperties(offsetManager); - return parseSavedOffset(debeziumProperties); - } - - private Optional parseSavedOffset(final Properties properties) { - FileOffsetBackingStore fileOffsetBackingStore = null; - OffsetStorageReaderImpl offsetStorageReader = null; - - try { - fileOffsetBackingStore = getFileOffsetBackingStore(properties); - offsetStorageReader = getOffsetStorageReader(fileOffsetBackingStore, properties); - - final MySqlConnectorConfig connectorConfig = new MySqlConnectorConfig(Configuration.from(properties)); - final MySqlOffsetContext.Loader loader = new MySqlOffsetContext.Loader(connectorConfig); - final Set partitions = - Collections.singleton(new MySqlPartition(connectorConfig.getLogicalName(), properties.getProperty(DATABASE_NAME.name()))); - - final OffsetReader offsetReader = new OffsetReader<>(offsetStorageReader, - loader); - final Map offsets = offsetReader.offsets(partitions); - - return extractStateAttributes(partitions, offsets); - } finally { - LOGGER.info("Closing offsetStorageReader and fileOffsetBackingStore"); - if (offsetStorageReader != null) { - offsetStorageReader.close(); - } - - if (fileOffsetBackingStore != null) { - fileOffsetBackingStore.stop(); - } - } - } - - private Optional extractStateAttributes(final Set partitions, - final Map offsets) { - boolean found = false; - for (final Partition partition : partitions) { - final MySqlOffsetContext mySqlOffsetContext = offsets.get(partition); - - if (mySqlOffsetContext != null) { - found = true; - LOGGER.info("Found previous partition offset {}: {}", partition, mySqlOffsetContext.getOffset()); - } - } - - if (!found) { - LOGGER.info("No previous offsets found"); - return Optional.empty(); - } - - final Offsets of = Offsets.of(offsets); - final MySqlOffsetContext previousOffset = of.getTheOnlyOffset(); - - return Optional.of(new MysqlDebeziumStateAttributes(previousOffset.getSource().binlogFilename(), previousOffset.getSource().binlogPosition(), - Optional.ofNullable(previousOffset.gtidSet()))); - - } - - public JsonNode constructInitialDebeziumState(final Properties properties, - final ConfiguredAirbyteCatalog catalog, - final JdbcDatabase database) { - // https://debezium.io/documentation/reference/2.2/connectors/mysql.html#mysql-property-snapshot-mode - // We use the recovery property cause using this mode will instruct Debezium to - // construct the db schema history. - // Note that we used to use schema_only_recovery mode, but this mode has been deprecated. - properties.setProperty("snapshot.mode", "recovery"); - final String dbName = database.getSourceConfig().get(JdbcUtils.DATABASE_KEY).asText(); - // Topic.prefix is sanitized version of database name. At this stage properties does not have this - // value - it's set in RelationalDbDebeziumPropertiesManager. - final AirbyteFileOffsetBackingStore offsetManager = AirbyteFileOffsetBackingStore.initializeState( - constructBinlogOffset(database, dbName, DebeziumPropertiesManager.sanitizeTopicPrefix(dbName)), - Optional.empty()); - final AirbyteSchemaHistoryStorage schemaHistoryStorage = - AirbyteSchemaHistoryStorage.initializeDBHistory(new SchemaHistory<>(Optional.empty(), false), COMPRESSION_ENABLED); - final LinkedBlockingQueue> queue = new LinkedBlockingQueue<>(); - final var debeziumPropertiesManager = - new RelationalDbDebeziumPropertiesManager(properties, database.getSourceConfig(), catalog, new ArrayList()); - - try (final DebeziumRecordPublisher publisher = new DebeziumRecordPublisher(debeziumPropertiesManager)) { - publisher.start(queue, offsetManager, Optional.of(schemaHistoryStorage)); - final Instant engineStartTime = Instant.now(); - while (!publisher.hasClosed()) { - final ChangeEvent event = queue.poll(10, TimeUnit.SECONDS); - if (event == null) { - Duration initialWaitingDuration = Duration.ofMinutes(5L); - // If initial waiting seconds is configured and it's greater than 5 minutes, use that value instead - // of the default value - final Duration configuredDuration = RecordWaitTimeUtil.getFirstRecordWaitTime(database.getSourceConfig()); - if (configuredDuration.compareTo(initialWaitingDuration) > 0) { - initialWaitingDuration = configuredDuration; - } - if (Duration.between(engineStartTime, Instant.now()).compareTo(initialWaitingDuration) > 0) { - LOGGER.error("No record is returned even after {} seconds of waiting, closing the engine", initialWaitingDuration.getSeconds()); - publisher.close(); - throw new RuntimeException( - "Building schema history has timed out. Please consider increasing the debezium wait time in advanced options."); - } - continue; - } - LOGGER.info("A record is returned, closing the engine since the state is constructed"); - publisher.close(); - break; - } - } catch (final Exception e) { - throw new RuntimeException(e); - } - - final Map offset = offsetManager.read(); - final SchemaHistory schemaHistory = schemaHistoryStorage.read(); - - assert !offset.isEmpty(); - assert Objects.nonNull(schemaHistory); - assert Objects.nonNull(schemaHistory.getSchema()); - - final JsonNode asJson = serialize(offset, schemaHistory); - LOGGER.info("Initial Debezium state constructed: {}", asJson); - - if (asJson.get(MysqlCdcStateConstants.MYSQL_DB_HISTORY).asText().isBlank()) { - throw new RuntimeException("Schema history snapshot returned empty history."); - } - return asJson; - } - - public static JsonNode serialize(final Map offset, final SchemaHistory dbHistory) { - final Map state = new HashMap<>(); - state.put(MysqlCdcStateConstants.MYSQL_CDC_OFFSET, offset); - state.put(MysqlCdcStateConstants.MYSQL_DB_HISTORY, dbHistory.getSchema()); - state.put(MysqlCdcStateConstants.IS_COMPRESSED, dbHistory.isCompressed()); - - return Jsons.jsonNode(state); - } - - /** - * Method to construct initial Debezium state which can be passed onto Debezium engine to make it - * process binlogs from a specific file and position and skip snapshot phase - */ - private JsonNode constructBinlogOffset(final JdbcDatabase database, final String debeziumName, final String topicPrefixName) { - return format(getStateAttributesFromDB(database), debeziumName, topicPrefixName, Instant.now()); - } - - @VisibleForTesting - public JsonNode format(final MysqlDebeziumStateAttributes attributes, final String debeziumName, final String topicPrefixName, final Instant time) { - final String key = "[\"" + debeziumName + "\",{\"server\":\"" + topicPrefixName + "\"}]"; - final String gtidSet = attributes.gtidSet().isPresent() ? ",\"gtids\":\"" + attributes.gtidSet().get() + "\"" : ""; - final String value = - "{\"transaction_id\":null,\"ts_sec\":" + time.getEpochSecond() + ",\"file\":\"" + attributes.binlogFilename() + "\",\"pos\":" - + attributes.binlogPosition() - + gtidSet + "}"; - - final Map result = new HashMap<>(); - result.put(key, value); - - final JsonNode jsonNode = Jsons.jsonNode(result); - LOGGER.info("Initial Debezium state offset constructed: {}", jsonNode); - - return jsonNode; - } - - public static MysqlDebeziumStateAttributes getStateAttributesFromDB(final JdbcDatabase database) { - try (final Stream stream = database.unsafeResultSetQuery( - connection -> connection.createStatement().executeQuery("SHOW MASTER STATUS"), - resultSet -> { - final String file = resultSet.getString("File"); - final long position = resultSet.getLong("Position"); - assert file != null; - assert position >= 0; - if (resultSet.getMetaData().getColumnCount() > 4) { - // This column exists only in MySQL 5.6.5 or later ... - final String gtidSet = resultSet.getString(5); // GTID set, may be null, blank, or contain a GTID set - return new MysqlDebeziumStateAttributes(file, position, removeNewLineChars(gtidSet)); - } - return new MysqlDebeziumStateAttributes(file, position, Optional.empty()); - })) { - final List stateAttributes = stream.toList(); - assert stateAttributes.size() == 1; - return stateAttributes.get(0); - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - private static Optional removeNewLineChars(final String gtidSet) { - if (gtidSet != null && !gtidSet.trim().isEmpty()) { - // Remove all the newline chars that exist in the GTID set string ... - return Optional.of(gtidSet.replace("\n", "").replace("\r", "")); - } - - return Optional.empty(); - } - - public record MysqlDebeziumStateAttributes(String binlogFilename, long binlogPosition, Optional gtidSet) { - - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MysqlCdcStateConstants.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MysqlCdcStateConstants.java deleted file mode 100644 index cac1bfd997d5..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cdc/MysqlCdcStateConstants.java +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cdc; - -public class MysqlCdcStateConstants { - - public static final String MYSQL_CDC_OFFSET = "mysql_cdc_offset"; - public static final String MYSQL_DB_HISTORY = "mysql_db_history"; - public static final String IS_COMPRESSED = "is_compressed"; - public static final boolean COMPRESSION_ENABLED = true; - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cursor_based/MySqlCursorBasedStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cursor_based/MySqlCursorBasedStateManager.java deleted file mode 100644 index 9741c9b2b7ca..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/cursor_based/MySqlCursorBasedStateManager.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.cursor_based; - -import com.google.common.collect.Lists; -import io.airbyte.cdk.integrations.source.relationaldb.CursorInfo; -import io.airbyte.cdk.integrations.source.relationaldb.state.StreamStateManager; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.internal.models.CursorBasedStatus; -import io.airbyte.integrations.source.mysql.internal.models.InternalModels.StateType; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import io.airbyte.protocol.models.v0.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.v0.AirbyteStreamState; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.StreamDescriptor; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlCursorBasedStateManager extends StreamStateManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlCursorBasedStateManager.class); - - public MySqlCursorBasedStateManager(final List airbyteStateMessages, final ConfiguredAirbyteCatalog catalog) { - super(airbyteStateMessages, catalog); - } - - @Override - public AirbyteStateMessage toState(final Optional pair) { - if (pair.isPresent()) { - final Map pairToCursorInfoMap = getPairToCursorInfoMap(); - final Optional cursorInfo = Optional.ofNullable(pairToCursorInfoMap.get(pair.get())); - - if (cursorInfo.isPresent()) { - LOGGER.debug("Generating state message for {}...", pair); - return new AirbyteStateMessage() - .withType(AirbyteStateType.STREAM) - // Temporarily include legacy state for backwards compatibility with the platform - .withStream(generateStreamState(pair.get(), cursorInfo.get())); - } else { - LOGGER.warn("Cursor information could not be located in state for stream {}. Returning a new, empty state message...", pair); - return new AirbyteStateMessage().withType(AirbyteStateType.STREAM).withStream(new AirbyteStreamState()); - } - } else { - LOGGER.warn("Stream not provided. Returning a new, empty state message..."); - return new AirbyteStateMessage().withType(AirbyteStateType.STREAM).withStream(new AirbyteStreamState()); - } - } - - /** - * Generates the stream state for the given stream and cursor information. - * - * @param airbyteStreamNameNamespacePair The stream. - * @param cursorInfo The current cursor. - * @return The {@link AirbyteStreamState} representing the current state of the stream. - */ - private AirbyteStreamState generateStreamState(final AirbyteStreamNameNamespacePair airbyteStreamNameNamespacePair, - final CursorInfo cursorInfo) { - return new AirbyteStreamState() - .withStreamDescriptor( - new StreamDescriptor().withName(airbyteStreamNameNamespacePair.getName()).withNamespace(airbyteStreamNameNamespacePair.getNamespace())) - .withStreamState(Jsons.jsonNode(generateDbStreamState(airbyteStreamNameNamespacePair, cursorInfo))); - } - - private CursorBasedStatus generateDbStreamState(final AirbyteStreamNameNamespacePair airbyteStreamNameNamespacePair, - final CursorInfo cursorInfo) { - final CursorBasedStatus state = new CursorBasedStatus(); - state.setStateType(StateType.CURSOR_BASED); - state.setVersion(2L); - state.setStreamName(airbyteStreamNameNamespacePair.getName()); - state.setStreamNamespace(airbyteStreamNameNamespacePair.getNamespace()); - state.setCursorField(cursorInfo.getCursorField() == null ? Collections.emptyList() : Lists.newArrayList(cursorInfo.getCursorField())); - state.setCursor(cursorInfo.getCursor()); - if (cursorInfo.getCursorRecordCount() > 0L) { - state.setCursorRecordCount(cursorInfo.getCursorRecordCount()); - } - return state; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/CdcMetadataInjector.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/CdcMetadataInjector.java deleted file mode 100644 index cde1f645a60f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/CdcMetadataInjector.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcConnectorMetadataInjector; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil.MysqlDebeziumStateAttributes; - -public class CdcMetadataInjector { - - private final String transactionTimestamp; - private final MysqlDebeziumStateAttributes stateAttributes; - private final MySqlCdcConnectorMetadataInjector metadataInjector; - - public CdcMetadataInjector(final String transactionTimestamp, - final MysqlDebeziumStateAttributes stateAttributes, - final MySqlCdcConnectorMetadataInjector metadataInjector) { - this.transactionTimestamp = transactionTimestamp; - this.stateAttributes = stateAttributes; - this.metadataInjector = metadataInjector; - } - - public void inject(final ObjectNode record) { - metadataInjector.addMetaDataToRowsFetchedOutsideDebezium(record, transactionTimestamp, stateAttributes); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java deleted file mode 100644 index 9b3de8d4047e..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadGlobalStateManager.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.integrations.source.relationaldb.models.CdcState; -import io.airbyte.cdk.integrations.source.relationaldb.models.DbStreamState; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManager; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.InitialLoadStreams; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.PrimaryKeyInfo; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.v0.AirbyteGlobalState; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import io.airbyte.protocol.models.v0.AirbyteStreamState; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.StreamDescriptor; -import io.airbyte.protocol.models.v0.SyncMode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlInitialLoadGlobalStateManager extends MySqlInitialLoadStateManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlInitialLoadGlobalStateManager.class); - protected StateManager stateManager; - - // Only one global state is emitted, which is fanned out into many entries in the DB by platform. As - // a result, we need to keep track of streams that - // have completed the snapshot. - private Set streamsThatHaveCompletedSnapshot; - - // No special handling for resumable full refresh streams. We will report the cursor as it is. - private Set resumableFullRefreshStreams; - - // non ResumableFullRefreshStreams do not have any state. We only report count for them. - private Set nonResumableFullRefreshStreams; - private Set completedNonResumableFullRefreshStreams; - - private final boolean savedOffsetStillPresentOnServer; - private final ConfiguredAirbyteCatalog catalog; - private final CdcState defaultCdcState; - - public MySqlInitialLoadGlobalStateManager(final InitialLoadStreams initialLoadStreams, - final Map pairToPrimaryKeyInfo, - final StateManager stateManager, - final ConfiguredAirbyteCatalog catalog, - - final boolean savedOffsetStillPresentOnServer, - final CdcState defaultCdcState) { - this.stateManager = stateManager; - this.pairToPrimaryKeyLoadStatus = MySqlInitialLoadStateManager.initPairToPrimaryKeyLoadStatusMap(initialLoadStreams.pairToInitialLoadStatus()); - this.pairToPrimaryKeyInfo = pairToPrimaryKeyInfo; - this.catalog = catalog; - this.savedOffsetStillPresentOnServer = savedOffsetStillPresentOnServer; - this.defaultCdcState = defaultCdcState; - this.streamStateForIncrementalRunSupplier = pair -> Jsons.emptyObject(); - initStreams(initialLoadStreams, catalog); - } - - private void initStreams(final InitialLoadStreams initialLoadStreams, - final ConfiguredAirbyteCatalog catalog) { - this.streamsThatHaveCompletedSnapshot = new HashSet<>(); - this.resumableFullRefreshStreams = new HashSet<>(); - this.nonResumableFullRefreshStreams = new HashSet<>(); - this.completedNonResumableFullRefreshStreams = new HashSet<>(); - - catalog.getStreams().forEach(configuredAirbyteStream -> { - var pairInStream = - new AirbyteStreamNameNamespacePair(configuredAirbyteStream.getStream().getName(), configuredAirbyteStream.getStream().getNamespace()); - if (!initialLoadStreams.streamsForInitialLoad().contains(configuredAirbyteStream) - && configuredAirbyteStream.getSyncMode() == SyncMode.INCREMENTAL) { - this.streamsThatHaveCompletedSnapshot.add(pairInStream); - } - if (configuredAirbyteStream.getSyncMode() == SyncMode.FULL_REFRESH) { - if (configuredAirbyteStream.getStream().getSourceDefinedPrimaryKey() != null - && !configuredAirbyteStream.getStream().getSourceDefinedPrimaryKey().isEmpty()) { - this.resumableFullRefreshStreams.add(pairInStream); - } else { - this.nonResumableFullRefreshStreams.add(pairInStream); - } - } - }); - } - - private AirbyteGlobalState generateGlobalState(final List streamStates) { - CdcState cdcState = stateManager.getCdcStateManager().getCdcState(); - - if (!savedOffsetStillPresentOnServer || cdcState == null - || cdcState.getState() == null) { - cdcState = defaultCdcState; - } - - final AirbyteGlobalState globalState = new AirbyteGlobalState(); - globalState.setSharedState(Jsons.jsonNode(cdcState)); - globalState.setStreamStates(streamStates); - return globalState; - } - - @Override - public AirbyteStateMessage generateStateMessageAtCheckpoint(final ConfiguredAirbyteStream airbyteStream) { - final List streamStates = new ArrayList<>(); - streamsThatHaveCompletedSnapshot.forEach(stream -> { - final DbStreamState state = getFinalState(stream); - streamStates.add(getAirbyteStreamState(stream, Jsons.jsonNode(state))); - }); - - resumableFullRefreshStreams.forEach(stream -> { - var pkStatus = getPrimaryKeyLoadStatus(stream); - if (pkStatus != null) { - streamStates.add(getAirbyteStreamState(stream, (Jsons.jsonNode(pkStatus)))); - } - }); - - completedNonResumableFullRefreshStreams.forEach(stream -> { - streamStates.add(new AirbyteStreamState() - .withStreamDescriptor( - new StreamDescriptor().withName(stream.getName()).withNamespace(stream.getNamespace()))); - }); - - if (airbyteStream.getSyncMode() == SyncMode.INCREMENTAL) { - AirbyteStreamNameNamespacePair pair = - new AirbyteStreamNameNamespacePair(airbyteStream.getStream().getName(), airbyteStream.getStream().getNamespace()); - var pkStatus = getPrimaryKeyLoadStatus(pair); - streamStates.add(getAirbyteStreamState(pair, (Jsons.jsonNode(pkStatus)))); - } - - return new AirbyteStateMessage() - .withType(AirbyteStateType.GLOBAL) - .withGlobal(generateGlobalState(streamStates)); - } - - @Override - public AirbyteStateMessage createFinalStateMessage(final ConfiguredAirbyteStream airbyteStream) { - final AirbyteStreamNameNamespacePair pair = - new AirbyteStreamNameNamespacePair(airbyteStream.getStream().getName(), airbyteStream.getStream().getNamespace()); - if (airbyteStream.getSyncMode() == SyncMode.INCREMENTAL) { - streamsThatHaveCompletedSnapshot.add(pair); - } else if (nonResumableFullRefreshStreams.contains(pair)) { - completedNonResumableFullRefreshStreams.add(pair); - } - final List streamStates = new ArrayList<>(); - - streamsThatHaveCompletedSnapshot.forEach(stream -> { - final DbStreamState state = getFinalState(stream); - streamStates.add(getAirbyteStreamState(stream, Jsons.jsonNode(state))); - }); - - resumableFullRefreshStreams.forEach(stream -> { - var pkStatus = getPrimaryKeyLoadStatus(stream); - streamStates.add(getAirbyteStreamState(stream, (Jsons.jsonNode(pkStatus)))); - }); - - completedNonResumableFullRefreshStreams.forEach(stream -> { - streamStates.add(new AirbyteStreamState() - .withStreamDescriptor( - new StreamDescriptor().withName(stream.getName()).withNamespace(stream.getNamespace()))); - }); - - return new AirbyteStateMessage() - .withType(AirbyteStateType.GLOBAL) - .withGlobal(generateGlobalState(streamStates)); - } - - @Override - public PrimaryKeyInfo getPrimaryKeyInfo(final AirbyteStreamNameNamespacePair pair) { - return pairToPrimaryKeyInfo.get(pair); - } - - private AirbyteStreamState getAirbyteStreamState(final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair, final JsonNode stateData) { - assert Objects.nonNull(pair); - assert Objects.nonNull(pair.getName()); - assert Objects.nonNull(pair.getNamespace()); - - return new AirbyteStreamState() - .withStreamDescriptor( - new StreamDescriptor().withName(pair.getName()).withNamespace(pair.getNamespace())) - .withStreamState(stateData); - } - - private DbStreamState getFinalState(final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair) { - assert Objects.nonNull(pair); - assert Objects.nonNull(pair.getName()); - assert Objects.nonNull(pair.getNamespace()); - - return new DbStreamState() - .withStreamName(pair.getName()) - .withStreamNamespace(pair.getNamespace()) - .withCursorField(Collections.emptyList()) - .withCursor(null); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadHandler.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadHandler.java deleted file mode 100644 index f9684c33ea1f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadHandler.java +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import static io.airbyte.cdk.integrations.debezium.DebeziumIteratorConstants.SYNC_CHECKPOINT_DURATION_PROPERTY; -import static io.airbyte.cdk.integrations.debezium.DebeziumIteratorConstants.SYNC_CHECKPOINT_RECORDS_PROPERTY; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.annotations.VisibleForTesting; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.jdbc.AirbyteRecordData; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.debezium.DebeziumIteratorConstants; -import io.airbyte.cdk.integrations.source.relationaldb.DbSourceDiscoverUtil; -import io.airbyte.cdk.integrations.source.relationaldb.InitialLoadHandler; -import io.airbyte.cdk.integrations.source.relationaldb.TableInfo; -import io.airbyte.cdk.integrations.source.relationaldb.state.SourceStateIterator; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateEmitFrequency; -import io.airbyte.cdk.integrations.source.relationaldb.streamstatus.StreamStatusTraceEmitterIterator; -import io.airbyte.commons.stream.AirbyteStreamStatusHolder; -import io.airbyte.commons.stream.AirbyteStreamUtils; -import io.airbyte.commons.util.AutoCloseableIterator; -import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mysql.MySqlQueryUtils.TableSizeInfo; -import io.airbyte.integrations.source.mysql.MySqlSourceOperations; -import io.airbyte.integrations.source.mysql.internal.models.PrimaryKeyLoadStatus; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.CommonField; -import io.airbyte.protocol.models.v0.*; -import io.airbyte.protocol.models.v0.AirbyteMessage.Type; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlInitialLoadHandler implements InitialLoadHandler { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlInitialLoadHandler.class); - - private static final long RECORD_LOGGING_SAMPLE_RATE = 1_000_000; - private final JsonNode config; - private final JdbcDatabase database; - private final MySqlSourceOperations sourceOperations; - private final String quoteString; - private final MySqlInitialLoadStateManager initialLoadStateManager; - private final Optional> streamStateForIncrementalRunSupplier; - - private static final long QUERY_TARGET_SIZE_GB = 1_073_741_824; - private static final long DEFAULT_CHUNK_SIZE = 1_000_000; - private long MAX_CHUNK_SIZE = Long.MAX_VALUE; - final Map tableSizeInfoMap; - - public MySqlInitialLoadHandler(final JsonNode config, - final JdbcDatabase database, - final MySqlSourceOperations sourceOperations, - final String quoteString, - final MySqlInitialLoadStateManager initialLoadStateManager, - final Optional> streamStateForIncrementalRunSupplier, - final Map tableSizeInfoMap) { - this.config = config; - this.database = database; - this.sourceOperations = sourceOperations; - this.quoteString = quoteString; - this.initialLoadStateManager = initialLoadStateManager; - this.streamStateForIncrementalRunSupplier = streamStateForIncrementalRunSupplier; - this.tableSizeInfoMap = tableSizeInfoMap; - adjustChunkSizeLimitForMySQLVariants(); - } - - private void adjustChunkSizeLimitForMySQLVariants() { - // For PSDB, we need to limit the chunk size to 100k rows to avoid the query being killed by the - // server. - // Reference: - // https://planetscale.com/docs/reference/planetscale-system-limits - if (config.get(JdbcUtils.HOST_KEY).asText().toLowerCase().contains("psdb.cloud")) - MAX_CHUNK_SIZE = 100_000; - } - - public List> getIncrementalIterators( - final ConfiguredAirbyteCatalog catalog, - final Map>> tableNameToTable, - final Instant emittedAt, - final boolean decorateWithStartedStatus, - final boolean decorateWithCompletedStatus, - final Optional cdcInitialLoadTimeout) { - final List> iteratorList = new ArrayList<>(); - for (final ConfiguredAirbyteStream airbyteStream : catalog.getStreams()) { - final AirbyteStream stream = airbyteStream.getStream(); - final String streamName = stream.getName(); - final String namespace = stream.getNamespace(); - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(streamName, namespace); - if (airbyteStream.getSyncMode().equals(SyncMode.INCREMENTAL)) { - final String fullyQualifiedTableName = DbSourceDiscoverUtil.getFullyQualifiedTableName(namespace, streamName); - final TableInfo> table = tableNameToTable.get(fullyQualifiedTableName); - if (decorateWithStartedStatus) { - iteratorList.add( - new StreamStatusTraceEmitterIterator(new AirbyteStreamStatusHolder(pair, AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.STARTED))); - } - - iteratorList.add(getIteratorForStream(airbyteStream, table, emittedAt, cdcInitialLoadTimeout)); - if (decorateWithCompletedStatus) { - iteratorList.add(new StreamStatusTraceEmitterIterator( - new AirbyteStreamStatusHolder(pair, AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.COMPLETE))); - } - } - } - return iteratorList; - } - - @Override - public AutoCloseableIterator getIteratorForStream( - @NotNull ConfiguredAirbyteStream airbyteStream, - @NotNull TableInfo> table, - @NotNull Instant emittedAt, - @NotNull final Optional cdcInitialLoadTimeout) { - - final AirbyteStream stream = airbyteStream.getStream(); - final String streamName = stream.getName(); - final String namespace = stream.getNamespace(); - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(streamName, namespace); - final List selectedDatabaseFields = table.getFields() - .stream() - .map(CommonField::getName) - .filter(CatalogHelpers.getTopLevelFieldNames(airbyteStream)::contains) - .collect(Collectors.toList()); - final AutoCloseableIterator queryStream = - new MySqlInitialLoadRecordIterator(database, sourceOperations, quoteString, initialLoadStateManager, selectedDatabaseFields, pair, - Long.min(calculateChunkSize(tableSizeInfoMap.get(pair), pair), MAX_CHUNK_SIZE), isCompositePrimaryKey(airbyteStream), emittedAt, - cdcInitialLoadTimeout); - final AutoCloseableIterator recordIterator = - getRecordIterator(queryStream, streamName, namespace, emittedAt.toEpochMilli()); - final AutoCloseableIterator recordAndMessageIterator = augmentWithState(recordIterator, airbyteStream, pair); - - return augmentWithLogs(recordAndMessageIterator, pair, streamName); - } - - private static boolean isCompositePrimaryKey(final ConfiguredAirbyteStream stream) { - return stream.getStream().getSourceDefinedPrimaryKey().size() > 1; - } - - // Calculates the number of rows to fetch per query. - @VisibleForTesting - public static long calculateChunkSize(final TableSizeInfo tableSizeInfo, final AirbyteStreamNameNamespacePair pair) { - // If table size info could not be calculated, a default chunk size will be provided. - if (tableSizeInfo == null || tableSizeInfo.tableSize() == 0 || tableSizeInfo.avgRowLength() == 0) { - LOGGER.info("Chunk size could not be determined for pair: {}, defaulting to {} rows", pair, DEFAULT_CHUNK_SIZE); - return DEFAULT_CHUNK_SIZE; - } - final long avgRowLength = tableSizeInfo.avgRowLength(); - final long chunkSize = QUERY_TARGET_SIZE_GB / avgRowLength; - LOGGER.info("Chunk size determined for pair: {}, is {}", pair, chunkSize); - return chunkSize; - } - - // Transforms the given iterator to create an {@link AirbyteRecordMessage} - private AutoCloseableIterator getRecordIterator( - final AutoCloseableIterator recordIterator, - final String streamName, - final String namespace, - final long emittedAt) { - return AutoCloseableIterators.transform(recordIterator, r -> new AirbyteMessage() - .withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage() - .withStream(streamName) - .withNamespace(namespace) - .withEmittedAt(emittedAt) - .withData(r.rawRowData()) - .withMeta(isMetaChangesEmptyOrNull(r.meta()) ? null : r.meta()))); - } - - private boolean isMetaChangesEmptyOrNull(AirbyteRecordMessageMeta meta) { - return meta == null || meta.getChanges() == null || meta.getChanges().isEmpty(); - } - - // Augments the given iterator with record count logs. - private AutoCloseableIterator augmentWithLogs(final AutoCloseableIterator iterator, - final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair, - final String streamName) { - final AtomicLong recordCount = new AtomicLong(); - return AutoCloseableIterators.transform(iterator, - AirbyteStreamUtils.convertFromNameAndNamespace(pair.getName(), pair.getNamespace()), - r -> { - final long count = recordCount.incrementAndGet(); - if (count % RECORD_LOGGING_SAMPLE_RATE == 0) { - LOGGER.info("Reading stream {}. Records read: {}", streamName, count); - } - return r; - }); - } - - private AutoCloseableIterator augmentWithState(final AutoCloseableIterator recordIterator, - final ConfiguredAirbyteStream airbyteStream, - final AirbyteStreamNameNamespacePair pair) { - - final PrimaryKeyLoadStatus currentPkLoadStatus = initialLoadStateManager.getPrimaryKeyLoadStatus(pair); - - final Duration syncCheckpointDuration = - config.get(SYNC_CHECKPOINT_DURATION_PROPERTY) != null ? Duration.ofSeconds(config.get(SYNC_CHECKPOINT_DURATION_PROPERTY).asLong()) - : DebeziumIteratorConstants.SYNC_CHECKPOINT_DURATION; - final Long syncCheckpointRecords = config.get(SYNC_CHECKPOINT_RECORDS_PROPERTY) != null ? config.get(SYNC_CHECKPOINT_RECORDS_PROPERTY).asLong() - : DebeziumIteratorConstants.SYNC_CHECKPOINT_RECORDS; - - if (streamStateForIncrementalRunSupplier.isPresent()) { - initialLoadStateManager.setStreamStateForIncrementalRunSupplier(streamStateForIncrementalRunSupplier.get()); - } - return AutoCloseableIterators.transformIterator( - r -> new SourceStateIterator<>(r, airbyteStream, initialLoadStateManager, - new StateEmitFrequency(syncCheckpointRecords, syncCheckpointDuration)), - recordIterator, pair); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadRecordIterator.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadRecordIterator.java deleted file mode 100644 index 1093560ad43d..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadRecordIterator.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import static io.airbyte.cdk.db.DbAnalyticsUtils.cdcSnapshotForceShutdownMessage; - -import com.google.common.collect.AbstractIterator; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.JdbcCompatibleSourceOperations; -import io.airbyte.cdk.db.jdbc.AirbyteRecordData; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.integrations.base.AirbyteTraceMessageUtility; -import io.airbyte.cdk.integrations.source.relationaldb.RelationalDbQueryUtils; -import io.airbyte.commons.exceptions.TransientErrorException; -import io.airbyte.commons.util.AutoCloseableIterator; -import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.PrimaryKeyInfo; -import io.airbyte.integrations.source.mysql.internal.models.PrimaryKeyLoadStatus; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.time.Duration; -import java.time.Instant; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; -import javax.annotation.CheckForNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This record iterator operates over a single stream. It continuously reads data from a table via - * multiple queries with the configured chunk size until the entire table is processed. The next - * query uses the highest watermark of the primary key seen in the previous subquery. Consider a - * table with chunk size = 1,000,000, and 3,500,000 records. The series of queries executed are : - * Query 1 : select * from table order by pk limit 1,800,000, pk_max = pk_max_1 Query 2 : select * - * from table where pk > pk_max_1 order by pk limit 1,800,000, pk_max = pk_max_2 Query 3 : select * - * from table where pk > pk_max_2 order by pk limit 1,800,000, pk_max = pk_max_3 Query 4 : select * - * from table where pk > pk_max_3 order by pk limit 1,800,000, pk_max = pk_max_4 Query 5 : select * - * from table where pk > pk_max_4 order by pk limit 1,800,000. Final query, since there are zero - * records processed here. - */ -@SuppressWarnings("try") -public class MySqlInitialLoadRecordIterator extends AbstractIterator - implements AutoCloseableIterator { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlInitialLoadRecordIterator.class); - - private final JdbcCompatibleSourceOperations sourceOperations; - - private final String quoteString; - private final MySqlInitialLoadStateManager initialLoadStateManager; - private final List columnNames; - private final AirbyteStreamNameNamespacePair pair; - private final JdbcDatabase database; - // Represents the number of rows to get with each query. - private final long chunkSize; - private final PrimaryKeyInfo pkInfo; - private final boolean isCompositeKeyLoad; - - private final Instant startInstant; - private int numSubqueries = 0; - private AutoCloseableIterator currentIterator; - - private Optional cdcInitialLoadTimeout; - private boolean isCdcSync; - - MySqlInitialLoadRecordIterator( - final JdbcDatabase database, - final JdbcCompatibleSourceOperations sourceOperations, - final String quoteString, - final MySqlInitialLoadStateManager initialLoadStateManager, - final List columnNames, - final AirbyteStreamNameNamespacePair pair, - final long chunkSize, - final boolean isCompositeKeyLoad, - final Instant startInstant, - final Optional cdcInitialLoadTimeout) { - this.database = database; - this.sourceOperations = sourceOperations; - this.quoteString = quoteString; - this.initialLoadStateManager = initialLoadStateManager; - this.columnNames = columnNames; - this.pair = pair; - this.chunkSize = chunkSize; - this.pkInfo = initialLoadStateManager.getPrimaryKeyInfo(pair); - this.isCompositeKeyLoad = isCompositeKeyLoad; - this.startInstant = startInstant; - this.cdcInitialLoadTimeout = cdcInitialLoadTimeout; - this.isCdcSync = isCdcSync(initialLoadStateManager); - } - - @CheckForNull - @Override - protected AirbyteRecordData computeNext() { - if (isCdcSync && cdcInitialLoadTimeout.isPresent() - && Duration.between(startInstant, Instant.now()).compareTo(cdcInitialLoadTimeout.get()) > 0) { - final String cdcInitialLoadTimeoutMessage = String.format( - "Initial load for table %s has taken longer than %s hours, Canceling sync so that CDC replication can catch-up on subsequent attempt, and then initial snapshotting will resume", - getAirbyteStream().get(), cdcInitialLoadTimeout.get().toHours()); - LOGGER.info(cdcInitialLoadTimeoutMessage); - AirbyteTraceMessageUtility.emitAnalyticsTrace(cdcSnapshotForceShutdownMessage()); - throw new TransientErrorException(cdcInitialLoadTimeoutMessage); - } - if (shouldBuildNextSubquery()) { - try { - // We will only issue one query for a composite key load. If we have already processed all the data - // associated with this - // query, we should indicate that we are done processing for the given stream. - if (isCompositeKeyLoad && numSubqueries >= 1) { - return endOfData(); - } - // Previous stream (and connection) must be manually closed in this iterator. - if (currentIterator != null) { - currentIterator.close(); - } - - LOGGER.info("Subquery number : {}", numSubqueries); - final Stream stream = database.unsafeQuery( - this::getPkPreparedStatement, sourceOperations::convertDatabaseRowToAirbyteRecordData); - - currentIterator = AutoCloseableIterators.fromStream(stream, pair); - numSubqueries++; - // If the current subquery has no records associated with it, the entire stream has been read. - if (!currentIterator.hasNext()) { - return endOfData(); - } - } catch (final Exception e) { - throw new RuntimeException(e); - } - } - return currentIterator.next(); - } - - private boolean shouldBuildNextSubquery() { - // The next sub-query should be built if (i) it is the first subquery in the sequence. (ii) the - // previous subquery has finished. - return (currentIterator == null || !currentIterator.hasNext()); - } - - private PreparedStatement getPkPreparedStatement(final Connection connection) { - try { - final String tableName = pair.getName(); - final String schemaName = pair.getNamespace(); - LOGGER.info("Preparing query for table: {}", tableName); - final String fullTableName = RelationalDbQueryUtils.getFullyQualifiedTableNameWithQuoting(schemaName, tableName, - quoteString); - - final String wrappedColumnNames = RelationalDbQueryUtils.enquoteIdentifierList(columnNames, quoteString); - - final PrimaryKeyLoadStatus pkLoadStatus = initialLoadStateManager.getPrimaryKeyLoadStatus(pair); - - if (pkLoadStatus == null) { - LOGGER.info("pkLoadStatus is null"); - final String quotedCursorField = RelationalDbQueryUtils.enquoteIdentifier(pkInfo.pkFieldName(), quoteString); - final String sql; - // We cannot load in chunks for a composite key load, since each field might not have distinct - // values. - if (isCompositeKeyLoad) { - sql = String.format("SELECT %s FROM %s ORDER BY %s", wrappedColumnNames, fullTableName, - quotedCursorField); - } else { - sql = String.format("SELECT %s FROM %s ORDER BY %s LIMIT %s", wrappedColumnNames, fullTableName, - quotedCursorField, chunkSize); - } - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - LOGGER.info("Executing query for table {}: {}", tableName, preparedStatement); - return preparedStatement; - } else { - LOGGER.info("pkLoadStatus value is : {}", pkLoadStatus.getPkVal()); - final String quotedCursorField = RelationalDbQueryUtils.enquoteIdentifier(pkLoadStatus.getPkName(), quoteString); - final String sql; - // We cannot load in chunks for a composite key load, since each field might not have distinct - // values. Furthermore, we have to issue a >= - // query since we may not have processed all of the data associated with the last saved primary key - // value. - if (isCompositeKeyLoad) { - sql = String.format("SELECT %s FROM %s WHERE %s >= ? ORDER BY %s", wrappedColumnNames, fullTableName, - quotedCursorField, quotedCursorField); - } else { - // The pk max value could be null - this can happen in the case of empty tables. In this case, we - // can just issue a query - // without any chunking. - if (pkInfo.pkMaxValue() != null) { - sql = String.format("SELECT %s FROM %s WHERE %s > ? AND %s <= ? ORDER BY %s LIMIT %s", wrappedColumnNames, fullTableName, - quotedCursorField, quotedCursorField, quotedCursorField, chunkSize); - } else { - sql = String.format("SELECT %s FROM %s WHERE %s > ? ORDER BY %s", wrappedColumnNames, fullTableName, - quotedCursorField, quotedCursorField); - } - } - final PreparedStatement preparedStatement = connection.prepareStatement(sql); - final MysqlType cursorFieldType = pkInfo.fieldType(); - sourceOperations.setCursorField(preparedStatement, 1, cursorFieldType, pkLoadStatus.getPkVal()); - if (!isCompositeKeyLoad && pkInfo.pkMaxValue() != null) { - sourceOperations.setCursorField(preparedStatement, 2, cursorFieldType, pkInfo.pkMaxValue()); - } - LOGGER.info("Executing query for table {}: {}", tableName, preparedStatement); - return preparedStatement; - } - } catch (final SQLException e) { - throw new RuntimeException(e); - } - } - - @Override - public void close() throws Exception { - if (currentIterator != null) { - currentIterator.close(); - } - } - - @Override - public Optional getAirbyteStream() { - return Optional.of(pair); - } - - private boolean isCdcSync(MySqlInitialLoadStateManager initialLoadStateManager) { - if (initialLoadStateManager instanceof MySqlInitialLoadGlobalStateManager) { - LOGGER.info("Running a cdc sync"); - return true; - } else { - LOGGER.info("Not running a cdc sync"); - return false; - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java deleted file mode 100644 index 6109191bcedf..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStateManager.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.integrations.source.relationaldb.state.SourceStateMessageProducer; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.PrimaryKeyInfo; -import io.airbyte.integrations.source.mysql.internal.models.InternalModels.StateType; -import io.airbyte.integrations.source.mysql.internal.models.PrimaryKeyLoadStatus; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; - -public abstract class MySqlInitialLoadStateManager implements SourceStateMessageProducer { - - public static final long MYSQL_STATUS_VERSION = 2; - public static final String STATE_TYPE_KEY = "state_type"; - public static final String PRIMARY_KEY_STATE_TYPE = "primary_key"; - - protected Function streamStateForIncrementalRunSupplier; - - protected Map pairToPrimaryKeyLoadStatus; - - // Map of pair to the primary key info (field name & data type) associated with it. - protected Map pairToPrimaryKeyInfo; - - void setStreamStateForIncrementalRunSupplier(final Function streamStateForIncrementalRunSupplier) { - this.streamStateForIncrementalRunSupplier = streamStateForIncrementalRunSupplier; - } - - // Updates the {@link PrimaryKeyLoadStatus} for the state associated with the given pair - public void updatePrimaryKeyLoadState(final AirbyteStreamNameNamespacePair pair, final PrimaryKeyLoadStatus pkLoadStatus) { - pairToPrimaryKeyLoadStatus.put(pair, pkLoadStatus); - } - - // Returns the previous state emitted, represented as a {@link PrimaryKeyLoadStatus} associated with - // the stream. - public PrimaryKeyLoadStatus getPrimaryKeyLoadStatus(final AirbyteStreamNameNamespacePair pair) { - return pairToPrimaryKeyLoadStatus.get(pair); - } - - // Returns the current {@PrimaryKeyInfo}, associated with the stream. This includes the data type & - // the column name associated with the stream. - public abstract PrimaryKeyInfo getPrimaryKeyInfo(final AirbyteStreamNameNamespacePair pair); - - protected JsonNode getIncrementalState(final AirbyteStreamNameNamespacePair pair) { - final PrimaryKeyLoadStatus currentPkLoadStatus = getPrimaryKeyLoadStatus(pair); - return (currentPkLoadStatus == null || currentPkLoadStatus.getIncrementalState() == null) ? streamStateForIncrementalRunSupplier.apply(pair) - : currentPkLoadStatus.getIncrementalState(); - } - - @Override - public AirbyteMessage processRecordMessage(final ConfiguredAirbyteStream stream, final AirbyteMessage message) { - if (Objects.nonNull(message)) { - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()); - final String pkFieldName = this.getPrimaryKeyInfo(pair).pkFieldName(); - final String lastPk = message.getRecord().getData().get(pkFieldName).asText(); - final PrimaryKeyLoadStatus pkStatus = new PrimaryKeyLoadStatus() - .withVersion(MYSQL_STATUS_VERSION) - .withStateType(StateType.PRIMARY_KEY) - .withPkName(pkFieldName) - .withPkVal(lastPk) - .withIncrementalState(getIncrementalState(pair)); - this.updatePrimaryKeyLoadState(pair, pkStatus); - } - return message; - } - - @Override - public boolean shouldEmitStateMessage(final ConfiguredAirbyteStream stream) { - return true; - } - - public static Map initPairToPrimaryKeyLoadStatusMap( - final Map pairToPkStatus) { - final Map map = new HashMap<>(); - pairToPkStatus.forEach((pair, pkStatus) -> { - final AirbyteStreamNameNamespacePair updatedPair = new AirbyteStreamNameNamespacePair(pair.getName(), pair.getNamespace()); - map.put(updatedPair, pkStatus); - }); - return map; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStreamStateManager.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStreamStateManager.java deleted file mode 100644 index 3cf91c569226..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialLoadStreamStateManager.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.InitialLoadStreams; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialReadUtil.PrimaryKeyInfo; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import io.airbyte.protocol.models.v0.AirbyteStreamState; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.StreamDescriptor; -import java.util.Map; -import java.util.Objects; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * This state manager extends the StreamStateManager to enable writing the state_type and version - * keys to the stream state when they're going through the iterator Once we have verified that - * expanding StreamStateManager itself to include this functionality, this class will be removed - */ -public class MySqlInitialLoadStreamStateManager extends MySqlInitialLoadStateManager { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlInitialLoadStreamStateManager.class); - - public MySqlInitialLoadStreamStateManager(final ConfiguredAirbyteCatalog catalog, - final InitialLoadStreams initialLoadStreams, - final Map pairToPrimaryKeyInfo) { - this.pairToPrimaryKeyInfo = pairToPrimaryKeyInfo; - this.pairToPrimaryKeyLoadStatus = MySqlInitialLoadStateManager.initPairToPrimaryKeyLoadStatusMap(initialLoadStreams.pairToInitialLoadStatus()); - this.streamStateForIncrementalRunSupplier = pair -> Jsons.emptyObject(); - } - - @Override - public AirbyteStateMessage createFinalStateMessage(final ConfiguredAirbyteStream stream) { - AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()); - final JsonNode incrementalState = getIncrementalState(pair); - if (incrementalState == null || incrementalState.isEmpty()) { - // resumeable full refresh - return generateStateMessageAtCheckpoint(stream); - } - - return new AirbyteStateMessage() - .withType(AirbyteStateType.STREAM) - .withStream(getAirbyteStreamState(pair, incrementalState)); - } - - @Override - public PrimaryKeyInfo getPrimaryKeyInfo(final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair) { - return pairToPrimaryKeyInfo.get(pair); - } - - @Override - public AirbyteStateMessage generateStateMessageAtCheckpoint(final ConfiguredAirbyteStream stream) { - AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()); - var pkStatus = getPrimaryKeyLoadStatus(pair); - return new AirbyteStateMessage() - .withType(AirbyteStateType.STREAM) - .withStream(getAirbyteStreamState(pair, Jsons.jsonNode(pkStatus))); - } - - protected AirbyteStreamState getAirbyteStreamState(final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair, final JsonNode stateData) { - LOGGER.info("STATE DATA FOR {}: {}", pair.getNamespace().concat("_").concat(pair.getName()), stateData); - assert Objects.nonNull(pair.getName()); - assert Objects.nonNull(pair.getNamespace()); - - return new AirbyteStreamState() - .withStreamDescriptor( - new StreamDescriptor().withName(pair.getName()).withNamespace(pair.getNamespace())) - .withStreamState(stateData); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialReadUtil.java b/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialReadUtil.java deleted file mode 100644 index 3a7d99c2805f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/java/io/airbyte/integrations/source/mysql/initialsync/MySqlInitialReadUtil.java +++ /dev/null @@ -1,582 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql.initialsync; - -import static io.airbyte.cdk.db.DbAnalyticsUtils.cdcCursorInvalidMessage; -import static io.airbyte.cdk.db.DbAnalyticsUtils.cdcResyncMessage; -import static io.airbyte.cdk.db.DbAnalyticsUtils.wassOccurrenceMessage; -import static io.airbyte.integrations.source.mysql.MySqlQueryUtils.getTableSizeInfoForStreams; -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.FAIL_SYNC_OPTION; -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.INVALID_CDC_CURSOR_POSITION_PROPERTY; -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.RESYNC_DATA_OPTION; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.MYSQL_CDC_OFFSET; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadGlobalStateManager.STATE_TYPE_KEY; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStateManager.PRIMARY_KEY_STATE_TYPE; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Sets; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.integrations.base.AirbyteTraceMessageUtility; -import io.airbyte.cdk.integrations.debezium.AirbyteDebeziumHandler; -import io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter; -import io.airbyte.cdk.integrations.debezium.internals.RecordWaitTimeUtil; -import io.airbyte.cdk.integrations.debezium.internals.RelationalDbDebeziumEventConverter; -import io.airbyte.cdk.integrations.debezium.internals.RelationalDbDebeziumPropertiesManager; -import io.airbyte.cdk.integrations.source.relationaldb.CdcStateManager; -import io.airbyte.cdk.integrations.source.relationaldb.DbSourceDiscoverUtil; -import io.airbyte.cdk.integrations.source.relationaldb.InitialLoadTimeoutUtil; -import io.airbyte.cdk.integrations.source.relationaldb.TableInfo; -import io.airbyte.cdk.integrations.source.relationaldb.models.CdcState; -import io.airbyte.cdk.integrations.source.relationaldb.state.StateManager; -import io.airbyte.cdk.integrations.source.relationaldb.streamstatus.StreamStatusTraceEmitterIterator; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.stream.AirbyteStreamStatusHolder; -import io.airbyte.commons.util.AutoCloseableIterator; -import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mysql.MySqlQueryUtils; -import io.airbyte.integrations.source.mysql.MySqlSourceOperations; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcConnectorMetadataInjector; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcPosition; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcProperties; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcSavedInfoFetcher; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcStateHandler; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcTargetPosition; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil.MysqlDebeziumStateAttributes; -import io.airbyte.integrations.source.mysql.internal.models.CursorBasedStatus; -import io.airbyte.integrations.source.mysql.internal.models.PrimaryKeyLoadStatus; -import io.airbyte.protocol.models.CommonField; -import io.airbyte.protocol.models.v0.*; -import java.time.Duration; -import java.time.Instant; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MySqlInitialReadUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger(MySqlInitialReadUtil.class); - - public static Optional getMySqlFullRefreshInitialLoadHandler(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final MySqlInitialLoadGlobalStateManager initialLoadStateManager, - final StateManager stateManager, - final ConfiguredAirbyteStream fullRefreshStream, - final Instant emittedAt, - final String quoteString, - final boolean savedOffsetStillPresentOnServer) { - final InitialLoadStreams initialLoadStreams = - cdcStreamsForInitialPrimaryKeyLoad(stateManager.getCdcStateManager(), catalog, savedOffsetStillPresentOnServer); - - // State manager will need to know all streams in order to produce a state message - // But for initial load handler we only want to produce iterator on the single full refresh stream. - if (!initialLoadStreams.streamsForInitialLoad().isEmpty()) { - - // Filter on initialLoadStream - final var pair = new AirbyteStreamNameNamespacePair(fullRefreshStream.getStream().getName(), fullRefreshStream.getStream().getNamespace()); - final var pkStatus = initialLoadStreams.pairToInitialLoadStatus.get(pair); - final Map fullRefreshPkStatus; - if (pkStatus == null) { - fullRefreshPkStatus = Map.of(); - } else { - fullRefreshPkStatus = Map.of(pair, pkStatus); - } - - var fullRefreshStreamInitialLoad = new InitialLoadStreams(List.of(fullRefreshStream), - fullRefreshPkStatus); - return Optional - .of(getMySqlInitialLoadHandler(database, emittedAt, quoteString, fullRefreshStreamInitialLoad, initialLoadStateManager, Optional.empty())); - } - return Optional.empty(); - } - - private static MySqlInitialLoadHandler getMySqlInitialLoadHandler( - final JdbcDatabase database, - final Instant emittedAt, - final String quoteString, - final InitialLoadStreams initialLoadStreams, - final MySqlInitialLoadStateManager initialLoadStateManager, - final Optional cdcMetadataInjector) { - final JsonNode sourceConfig = database.getSourceConfig(); - - final MySqlSourceOperations sourceOperations = - new MySqlSourceOperations(cdcMetadataInjector); - return new MySqlInitialLoadHandler(sourceConfig, database, - sourceOperations, - quoteString, - initialLoadStateManager, - Optional.empty(), - getTableSizeInfoForStreams(database, initialLoadStreams.streamsForInitialLoad(), quoteString)); - } - - private static CdcState getDefaultCdcState(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog) { - - // Construct the initial state for MySQL. If there is already existing state, we use that instead - // since that is associated with the debezium - // state associated with the initial sync. - final MySqlDebeziumStateUtil mySqlDebeziumStateUtil = new MySqlDebeziumStateUtil(); - final JsonNode initialDebeziumState = mySqlDebeziumStateUtil.constructInitialDebeziumState( - MySqlCdcProperties.getDebeziumProperties(database), catalog, database); - return new CdcState().withState(initialDebeziumState); - } - - public static boolean isSavedOffsetStillPresentOnServer(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final StateManager stateManager) { - final MySqlDebeziumStateUtil mySqlDebeziumStateUtil = new MySqlDebeziumStateUtil(); - final JsonNode sourceConfig = database.getSourceConfig(); - final JsonNode initialDebeziumState = mySqlDebeziumStateUtil.constructInitialDebeziumState( - MySqlCdcProperties.getDebeziumProperties(database), catalog, database); - - final JsonNode state = - (stateManager.getCdcStateManager().getCdcState() == null || stateManager.getCdcStateManager().getCdcState().getState() == null) - ? initialDebeziumState - : Jsons.clone(stateManager.getCdcStateManager().getCdcState().getState()); - - final Optional savedOffset = mySqlDebeziumStateUtil.savedOffset( - MySqlCdcProperties.getDebeziumProperties(database), catalog, state.get(MYSQL_CDC_OFFSET), sourceConfig); - - final boolean savedOffsetStillPresentOnServer = - savedOffset.isPresent() && mySqlDebeziumStateUtil.savedOffsetStillPresentOnServer(database, savedOffset.get()); - if (!savedOffsetStillPresentOnServer) { - AirbyteTraceMessageUtility.emitAnalyticsTrace(cdcCursorInvalidMessage()); - if (!sourceConfig.get("replication_method").has(INVALID_CDC_CURSOR_POSITION_PROPERTY) || sourceConfig.get("replication_method").get( - INVALID_CDC_CURSOR_POSITION_PROPERTY).asText().equals(FAIL_SYNC_OPTION)) { - throw new ConfigErrorException( - "Saved offset no longer present on the server. Please reset the connection, and then increase binlog retention and/or increase sync frequency. See https://docs.airbyte.com/integrations/sources/mysql/mysql-troubleshooting#under-cdc-incremental-mode-there-are-still-full-refresh-syncs for more details."); - } else if (sourceConfig.get("replication_method").get(INVALID_CDC_CURSOR_POSITION_PROPERTY).asText().equals(RESYNC_DATA_OPTION)) { - AirbyteTraceMessageUtility.emitAnalyticsTrace(cdcResyncMessage()); - LOGGER.warn("Saved offset no longer present on the server, Airbyte is going to trigger a sync from scratch"); - } - } - return savedOffsetStillPresentOnServer; - } - - public static MySqlInitialLoadGlobalStateManager getMySqlInitialLoadGlobalStateManager(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final StateManager stateManager, - final Map>> tableNameToTable, - final String quoteString, - final boolean savedOffsetStillPresentOnServer) { - final InitialLoadStreams initialLoadStreams = - cdcStreamsForInitialPrimaryKeyLoad(stateManager.getCdcStateManager(), catalog, savedOffsetStillPresentOnServer); - - return new MySqlInitialLoadGlobalStateManager(initialLoadStreams, - initPairToPrimaryKeyInfoMap(database, catalog, tableNameToTable, quoteString), - stateManager, catalog, savedOffsetStillPresentOnServer, getDefaultCdcState(database, catalog)); - } - - /* - * Returns the read iterators associated with : 1. Initial cdc read snapshot via primary key - * queries. 2. Incremental cdc reads via debezium. - * - * The initial load iterators need to always be run before the incremental cdc iterators. This is to - * prevent advancing the binlog offset in the state before all streams have snapshotted. Otherwise, - * there could be data loss. - */ - public static List> getCdcReadIterators(final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final Map>> tableNameToTable, - final StateManager stateManager, - final MySqlInitialLoadGlobalStateManager initialLoadGlobalStateManager, - final Instant emittedAt, - final String quoteString, - final boolean savedOffsetStillPresentOnServer) { - final JsonNode sourceConfig = database.getSourceConfig(); - final Duration firstRecordWaitTime = RecordWaitTimeUtil.getFirstRecordWaitTime(sourceConfig); - LOGGER.info("First record waiting time: {} seconds", firstRecordWaitTime.getSeconds()); - final Duration initialLoadTimeout = InitialLoadTimeoutUtil.getInitialLoadTimeout(sourceConfig); - // Determine the streams that need to be loaded via primary key sync. - final List> initialLoadIterator = new ArrayList<>(); - final InitialLoadStreams initialLoadStreams = - cdcStreamsForInitialPrimaryKeyLoad(stateManager.getCdcStateManager(), catalog, savedOffsetStillPresentOnServer); - - final MySqlCdcConnectorMetadataInjector metadataInjector = MySqlCdcConnectorMetadataInjector.getInstance(emittedAt); - final CdcState stateToBeUsed; - final CdcState cdcState = stateManager.getCdcStateManager().getCdcState(); - if (!savedOffsetStillPresentOnServer || cdcState == null - || cdcState.getState() == null) { - stateToBeUsed = getDefaultCdcState(database, catalog); - } else { - stateToBeUsed = cdcState; - } - - // Debezium is started for streams that have been started - that is they have been partially or - // fully completed. - final var startedCdcStreamList = catalog.getStreams().stream() - .filter(stream -> stream.getSyncMode() == SyncMode.INCREMENTAL) - .filter(stream -> isStreamPartiallyOrFullyCompleted(stream, initialLoadStreams)) - .map(stream -> stream.getStream().getNamespace() + "." + stream.getStream().getName()).toList(); - - final var allCdcStreamList = catalog.getStreams().stream() - .filter(stream -> stream.getSyncMode() == SyncMode.INCREMENTAL) - .map(stream -> stream.getStream().getNamespace() + "." + stream.getStream().getName()).toList(); - - // If there are streams to sync via primary key load, build the relevant iterators. - if (!initialLoadStreams.streamsForInitialLoad().isEmpty()) { - - final MysqlDebeziumStateAttributes stateAttributes = MySqlDebeziumStateUtil.getStateAttributesFromDB(database); - - final MySqlInitialLoadHandler initialLoadHandler = - getMySqlInitialLoadHandler(database, emittedAt, quoteString, initialLoadStreams, initialLoadGlobalStateManager, - Optional.of(new CdcMetadataInjector(emittedAt.toString(), stateAttributes, metadataInjector))); - - // Start and complete stream status messages are emitted while constructing the full set of initial - // load and incremental debezium iterators. - initialLoadIterator.addAll(initialLoadHandler.getIncrementalIterators( - new ConfiguredAirbyteCatalog().withStreams(initialLoadStreams.streamsForInitialLoad()), - tableNameToTable, - emittedAt, false, false, Optional.of(initialLoadTimeout))); - } - - // CDC stream status messages should be emitted for streams. - final List> cdcStreamsStartStatusEmitters = catalog.getStreams().stream() - .filter(stream -> stream.getSyncMode() == SyncMode.INCREMENTAL) - .map(stream -> (AutoCloseableIterator) new StreamStatusTraceEmitterIterator( - new AirbyteStreamStatusHolder( - new io.airbyte.protocol.models.AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()), - AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.STARTED))) - .toList(); - - final List> cdcStreamsEndStatusEmitters = catalog.getStreams().stream() - .filter(stream -> stream.getSyncMode() == SyncMode.INCREMENTAL) - .map(stream -> (AutoCloseableIterator) new StreamStatusTraceEmitterIterator( - new AirbyteStreamStatusHolder( - new io.airbyte.protocol.models.AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()), - AirbyteStreamStatusTraceMessage.AirbyteStreamStatus.COMPLETE))) - .toList(); - - // Build the incremental CDC iterators. - final AirbyteDebeziumHandler handler = new AirbyteDebeziumHandler( - sourceConfig, - MySqlCdcTargetPosition.targetPosition(database), - true, - firstRecordWaitTime, - AirbyteDebeziumHandler.QUEUE_CAPACITY, - false); - final var eventConverter = new RelationalDbDebeziumEventConverter(metadataInjector, emittedAt); - - if (startedCdcStreamList.isEmpty()) { - LOGGER.info("First sync - no cdc streams have been completed or started"); - /* - * This is the first run case - no initial loads have been started. In this case, we want to run the - * iterators in the following order: 1. Run the initial load iterators. This step will timeout and - * throw a transient error if run for too long (> 8hrs by default). 2. Run the debezium iterators - * with ALL of the incremental streams configured. This is because if step 1 completes, the initial - * load can be considered finished. - */ - final var propertiesManager = new RelationalDbDebeziumPropertiesManager( - MySqlCdcProperties.getDebeziumProperties(database), sourceConfig, catalog, allCdcStreamList); - final Supplier> incrementalIteratorsSupplier = getCdcIncrementalIteratorsSupplier(handler, - propertiesManager, eventConverter, stateToBeUsed, stateManager); - return Collections.singletonList( - AutoCloseableIterators.concatWithEagerClose( - Stream - .of( - cdcStreamsStartStatusEmitters, - initialLoadIterator, - Collections.singletonList(AutoCloseableIterators.lazyIterator(incrementalIteratorsSupplier, null)), - cdcStreamsEndStatusEmitters) - .flatMap(Collection::stream) - .collect(Collectors.toList()), - AirbyteTraceMessageUtility::emitStreamStatusTrace)); - } else if (initialLoadIterator.isEmpty()) { - LOGGER.info("Initial load has finished completely - only reading the binlog"); - /* - * In this case, the initial load has completed and only debezium should be run. The iterators - * should be run in the following order: 1. Run the debezium iterators with ALL of the incremental - * streams configured. - */ - final var propertiesManager = new RelationalDbDebeziumPropertiesManager( - MySqlCdcProperties.getDebeziumProperties(database), sourceConfig, catalog, allCdcStreamList); - final Supplier> incrementalIteratorSupplier = getCdcIncrementalIteratorsSupplier(handler, - propertiesManager, eventConverter, stateToBeUsed, stateManager); - return Collections.singletonList( - AutoCloseableIterators.concatWithEagerClose( - Stream - .of( - cdcStreamsStartStatusEmitters, - Collections.singletonList(AutoCloseableIterators.lazyIterator(incrementalIteratorSupplier, null)), - cdcStreamsEndStatusEmitters) - .flatMap(Collection::stream) - .collect(Collectors.toList()), - AirbyteTraceMessageUtility::emitStreamStatusTrace)); - } else { - LOGGER.info("Initial load is in progress - reading binlog first and then resuming with initial load."); - /* - * In this case, the initial load has partially completed (WASS case). The iterators should be run - * in the following order: 1. Run the debezium iterators with only the incremental streams which - * have been fully or partially completed configured. 2. Resume initial load for partially completed - * and not started streams. This step will timeout and throw a transient error if run for too long - * (> 8hrs by default). - */ - AirbyteTraceMessageUtility.emitAnalyticsTrace(wassOccurrenceMessage()); - final var propertiesManager = new RelationalDbDebeziumPropertiesManager( - MySqlCdcProperties.getDebeziumProperties(database), sourceConfig, catalog, startedCdcStreamList); - final Supplier> incrementalIteratorSupplier = getCdcIncrementalIteratorsSupplier(handler, - propertiesManager, eventConverter, stateToBeUsed, stateManager); - return Collections.singletonList( - AutoCloseableIterators.concatWithEagerClose( - Stream - .of( - cdcStreamsStartStatusEmitters, - Collections.singletonList(AutoCloseableIterators.lazyIterator(incrementalIteratorSupplier, null)), - initialLoadIterator, - cdcStreamsEndStatusEmitters) - .flatMap(Collection::stream) - .collect(Collectors.toList()), - AirbyteTraceMessageUtility::emitStreamStatusTrace)); - } - } - - @SuppressWarnings("unchecked") - private static Supplier> getCdcIncrementalIteratorsSupplier(AirbyteDebeziumHandler handler, - RelationalDbDebeziumPropertiesManager propertiesManager, - DebeziumEventConverter eventConverter, - CdcState stateToBeUsed, - StateManager stateManager) { - return () -> handler.getIncrementalIterators( - propertiesManager, eventConverter, new MySqlCdcSavedInfoFetcher(stateToBeUsed), new MySqlCdcStateHandler(stateManager)); - } - - /** - * CDC specific: Determines the streams to sync for initial primary key load. These include streams - * that are (i) currently in primary key load (ii) newly added incremental streams. - */ - public static InitialLoadStreams cdcStreamsForInitialPrimaryKeyLoad(final CdcStateManager stateManager, - final ConfiguredAirbyteCatalog fullCatalog, - final boolean savedOffsetStillPresentOnServer) { - - if (!savedOffsetStillPresentOnServer) { - // Add a filter here to identify resumable full refresh streams. - return new InitialLoadStreams( - fullCatalog.getStreams() - .stream() - .collect(Collectors.toList()), - new HashMap<>()); - } - - final AirbyteStateMessage airbyteStateMessage = stateManager.getRawStateMessage(); - final Set streamsStillinPkSync = new HashSet<>(); - - // Build a map of stream <-> initial load status for streams that currently have an initial primary - // key load in progress. - final Map pairToInitialLoadStatus = new HashMap<>(); - if (airbyteStateMessage != null && airbyteStateMessage.getGlobal() != null && airbyteStateMessage.getGlobal().getStreamStates() != null) { - airbyteStateMessage.getGlobal().getStreamStates().forEach(stateMessage -> { - final JsonNode streamState = stateMessage.getStreamState(); - final StreamDescriptor streamDescriptor = stateMessage.getStreamDescriptor(); - if (streamState == null || streamDescriptor == null) { - return; - } - - if (streamState.has(STATE_TYPE_KEY)) { - if (streamState.get(STATE_TYPE_KEY).asText().equalsIgnoreCase(PRIMARY_KEY_STATE_TYPE)) { - final PrimaryKeyLoadStatus primaryKeyLoadStatus = Jsons.object(streamState, PrimaryKeyLoadStatus.class); - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(streamDescriptor.getName(), - streamDescriptor.getNamespace()); - pairToInitialLoadStatus.put(pair, primaryKeyLoadStatus); - streamsStillinPkSync.add(pair); - } - } - }); - } - - final List streamsForPkSync = new ArrayList<>(); - fullCatalog.getStreams().stream() - .filter(stream -> streamsStillinPkSync.contains(AirbyteStreamNameNamespacePair.fromAirbyteStream(stream.getStream()))) - .map(Jsons::clone) - .forEach(streamsForPkSync::add); - final List newlyAddedStreams = - identifyStreamsToSnapshot(fullCatalog, stateManager.getInitialStreamsSynced()); - streamsForPkSync.addAll(newlyAddedStreams); - - return new InitialLoadStreams(streamsForPkSync, pairToInitialLoadStatus); - } - - /** - * Determines the streams to sync for initial primary key load. These include streams that are (i) - * currently in primary key load (ii) newly added incremental streams. - */ - public static InitialLoadStreams streamsForInitialPrimaryKeyLoad(final StateManager stateManager, - final ConfiguredAirbyteCatalog fullCatalog) { - - final List rawStateMessages = stateManager.getRawStateMessages(); - final Set streamsStillInPkSync = new HashSet<>(); - final Set alreadySeenStreamPairs = new HashSet<>(); - - // Build a map of stream <-> initial load status for streams that currently have an initial primary - // key load in progress. - final Map pairToInitialLoadStatus = new HashMap<>(); - - if (rawStateMessages != null) { - rawStateMessages.forEach(stateMessage -> { - final AirbyteStreamState stream = stateMessage.getStream(); - final JsonNode streamState = stream.getStreamState(); - final StreamDescriptor streamDescriptor = stateMessage.getStream().getStreamDescriptor(); - if (streamState == null || streamDescriptor == null) { - return; - } - - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair(streamDescriptor.getName(), - streamDescriptor.getNamespace()); - - // Build a map of stream <-> initial load status for streams that currently have an initial primary - // key load in progress. - - if (streamState.has(STATE_TYPE_KEY)) { - if (streamState.get(STATE_TYPE_KEY).asText().equalsIgnoreCase(PRIMARY_KEY_STATE_TYPE)) { - final PrimaryKeyLoadStatus primaryKeyLoadStatus = Jsons.object(streamState, PrimaryKeyLoadStatus.class); - pairToInitialLoadStatus.put(pair, primaryKeyLoadStatus); - streamsStillInPkSync.add(pair); - } - alreadySeenStreamPairs.add(new AirbyteStreamNameNamespacePair(streamDescriptor.getName(), streamDescriptor.getNamespace())); - } - }); - } - final List streamsForPkSync = new ArrayList<>(); - fullCatalog.getStreams().stream() - .filter(stream -> streamsStillInPkSync.contains(AirbyteStreamNameNamespacePair.fromAirbyteStream(stream.getStream()))) - .map(Jsons::clone) - .forEach(streamsForPkSync::add); - - final List newlyAddedStreams = identifyStreamsToSnapshot(fullCatalog, - Collections.unmodifiableSet(alreadySeenStreamPairs)); - streamsForPkSync.addAll(newlyAddedStreams); - return new InitialLoadStreams(streamsForPkSync.stream().filter(MySqlInitialReadUtil::streamHasPrimaryKey).collect(Collectors.toList()), - pairToInitialLoadStatus); - } - - private static boolean streamHasPrimaryKey(final ConfiguredAirbyteStream stream) { - return stream.getStream().getSourceDefinedPrimaryKey().size() > 0; - } - - public static InitialLoadStreams filterStreamInIncrementalMode(final InitialLoadStreams stream) { - return new InitialLoadStreams( - stream.streamsForInitialLoad.stream().filter(airbyteStream -> airbyteStream.getSyncMode() == SyncMode.INCREMENTAL) - .collect(Collectors.toList()), - stream.pairToInitialLoadStatus); - } - - public static List identifyStreamsToSnapshot(final ConfiguredAirbyteCatalog catalog, - final Set alreadySyncedStreams) { - final Set allStreams = AirbyteStreamNameNamespacePair.fromConfiguredCatalog(catalog); - final Set newlyAddedStreams = new HashSet<>(Sets.difference(allStreams, alreadySyncedStreams)); - // Add a filter here to exclude non resumable full refresh streams. - return catalog.getStreams().stream() - .filter(stream -> newlyAddedStreams.contains(AirbyteStreamNameNamespacePair.fromAirbyteStream(stream.getStream()))) - .map(Jsons::clone) - .collect(Collectors.toList()); - } - - public static List identifyStreamsForCursorBased(final ConfiguredAirbyteCatalog catalog, - final List streamsForInitialLoad) { - - final Set initialLoadStreamsNamespacePairs = - streamsForInitialLoad.stream().map(stream -> AirbyteStreamNameNamespacePair.fromAirbyteStream(stream.getStream())) - .collect( - Collectors.toSet()); - return catalog.getStreams().stream() - .filter(stream -> !initialLoadStreamsNamespacePairs.contains(AirbyteStreamNameNamespacePair.fromAirbyteStream(stream.getStream()))) - .map(Jsons::clone) - .collect(Collectors.toList()); - } - - // Build a map of stream <-> primary key info (primary key field name + datatype) for all streams - // currently undergoing initial primary key syncs. - public static Map initPairToPrimaryKeyInfoMap( - final JdbcDatabase database, - final ConfiguredAirbyteCatalog catalog, - final Map>> tableNameToTable, - final String quoteString) { - final Map pairToPkInfoMap = new HashMap<>(); - // For every stream that was in primary initial key sync, we want to maintain information about the - // current primary key info associated with the - // stream - catalog.getStreams().forEach(stream -> { - final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair pair = - new io.airbyte.protocol.models.AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()); - final Optional pkInfo = getPrimaryKeyInfo(database, stream, tableNameToTable, quoteString); - if (pkInfo.isPresent()) { - pairToPkInfoMap.put(pair, pkInfo.get()); - } - }); - return pairToPkInfoMap; - } - - // Returns the primary key info associated with the stream. - private static Optional getPrimaryKeyInfo(final JdbcDatabase database, - final ConfiguredAirbyteStream stream, - final Map>> tableNameToTable, - final String quoteString) { - final String fullyQualifiedTableName = - DbSourceDiscoverUtil.getFullyQualifiedTableName(stream.getStream().getNamespace(), (stream.getStream().getName())); - final TableInfo> table = tableNameToTable - .get(fullyQualifiedTableName); - return getPrimaryKeyInfo(database, stream, table, quoteString); - } - - private static Optional getPrimaryKeyInfo(final JdbcDatabase database, - final ConfiguredAirbyteStream stream, - final TableInfo> table, - final String quoteString) { - // For cursor-based syncs, we cannot always assume a primary key field exists. We need to handle the - // case where it does not exist when we support - // cursor-based syncs. - if (stream.getStream().getSourceDefinedPrimaryKey().size() > 1) { - LOGGER.info("Composite primary key detected for {namespace, stream} : {}, {}", stream.getStream().getNamespace(), stream.getStream().getName()); - } - if (stream.getStream().getSourceDefinedPrimaryKey().isEmpty()) { - return Optional.empty(); - } - - final String pkFieldName = stream.getStream().getSourceDefinedPrimaryKey().getFirst().getFirst(); - final MysqlType pkFieldType = table.getFields().stream() - .filter(field -> field.getName().equals(pkFieldName)) - .findFirst().get().getType(); - - final String pkMaxValue = MySqlQueryUtils.getMaxPkValueForStream(database, stream, pkFieldName, quoteString); - return Optional.of(new PrimaryKeyInfo(pkFieldName, pkFieldType, pkMaxValue)); - } - - private static boolean isStreamPartiallyOrFullyCompleted(ConfiguredAirbyteStream stream, InitialLoadStreams initialLoadStreams) { - boolean isStreamCompleted = !initialLoadStreams.streamsForInitialLoad.contains(stream); - // A stream has been partially completed if an initial load status exists. - boolean isStreamPartiallyCompleted = (initialLoadStreams.pairToInitialLoadStatus - .get(new AirbyteStreamNameNamespacePair(stream.getStream().getName(), stream.getStream().getNamespace()))) != null; - return isStreamCompleted || isStreamPartiallyCompleted; - } - - public record InitialLoadStreams(List streamsForInitialLoad, - Map pairToInitialLoadStatus) { - - } - - public record CursorBasedStreams(List streamsForCursorBased, - Map pairToCursorBasedStatus) { - - } - - public record PrimaryKeyInfo(String pkFieldName, MysqlType fieldType, String pkMaxValue) {} - - public static AirbyteStreamNameNamespacePair convertNameNamespacePairFromV0(final io.airbyte.protocol.models.AirbyteStreamNameNamespacePair v1NameNamespacePair) { - return new AirbyteStreamNameNamespacePair(v1NameNamespacePair.getName(), v1NameNamespacePair.getNamespace()); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/main/kotlin/MySqlSourceExceptionHandler.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/MySqlSourceExceptionHandler.kt deleted file mode 100644 index 7853afc08fb4..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/kotlin/MySqlSourceExceptionHandler.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2024 Airbyte, Inc., all rights reserved. - */ -package io.airbyte.integrations.source.mysql - -import io.airbyte.cdk.integrations.util.ConnectorErrorProfile -import io.airbyte.cdk.integrations.util.ConnectorExceptionHandler -import io.airbyte.cdk.integrations.util.FailureType - -class MySqlSourceExceptionHandler : ConnectorExceptionHandler() { - override fun initializeErrorDictionary() { - // adding common error profiles - super.initializeErrorDictionary() - // adding connector specific error profiles - add( - ConnectorErrorProfile( - errorClass = "MySQL Syntax Exception", - regexMatchingPattern = ".*unknown column '.+' in 'field list'.*", - failureType = FailureType.CONFIG, - externalMessage = - "A column needed by MySQL source connector is missing in the database", - sampleInternalMessage = "Unknown column 'X' in 'field list'", - ), - ) - - add( - ConnectorErrorProfile( - errorClass = "MySQL EOF Exception", - regexMatchingPattern = - ".*can not read response from server. expected to read [1-9]\\d* bytes.*", - failureType = FailureType.TRANSIENT, - externalMessage = "Can not read data from MySQL server", - sampleInternalMessage = - "java.io.EOFException: Can not read response from server. Expected to read X bytes, read Y bytes before connection was unexpectedly lost.", - ), - ) - - add( - ConnectorErrorProfile( - errorClass = "MySQL Hikari Connection Error", - regexMatchingPattern = ".*connection is not available, request timed out after*", - failureType = FailureType.TRANSIENT, - externalMessage = "Database read failed due to connection timeout, will retry.", - sampleInternalMessage = - "java.sql.SQLTransientConnectionException: HikariPool-x - Connection is not available, request timed out after xms", - referenceLinks = listOf("https://github.com/airbytehq/airbyte/issues/41614"), - ), - ) - - add( - ConnectorErrorProfile( - errorClass = "MySQL Timezone Error", - regexMatchingPattern = ".*is unrecognized or represents more than one time zone*", - failureType = FailureType.CONFIG, - externalMessage = - "Please configure your database with the correct timezone found in the detailed error message. " + - "Please refer to the following documentation: https://dev.mysql.com/doc/refman/8.4/en/time-zone-support.html", - sampleInternalMessage = - "java.lang.RuntimeException: Connector configuration is not valid. Unable to connect: " + - "The server time zone value 'PDT' is unrecognized or represents more than one time zone. " + - "You must configure either the server or JDBC driver (via the 'connectionTimeZone' configuration property) to " + - "use a more specific time zone value if you want to utilize time zone support.", - referenceLinks = - listOf( - "https://github.com/airbytehq/airbyte/issues/41614", - "https://github.com/airbytehq/oncall/issues/5250", - ), - ), - ) - - add( - ConnectorErrorProfile( - errorClass = "MySQL Schema change error", - regexMatchingPattern = ".*whose schema isn't known to this connector*", - failureType = FailureType.CONFIG, - externalMessage = - "Your connection could not be completed because changes were detected on an unknown table (see detailed error for the table name), " + - "please refresh your schema or reset the connection.", - sampleInternalMessage = - "java.lang.RuntimeException: java.lang.RuntimeException: org.apache.kafka.connect.errors." + - "ConnectException: An exception occurred in the change event producer. This connector will be stopped.", - referenceLinks = - listOf("https://github.com/airbytehq/airbyte-internal-issues/issues/7156"), - ), - ) - - add( - ConnectorErrorProfile( - errorClass = "MySQL limit reached", - regexMatchingPattern = - ".*query execution was interrupted, maximum statement execution time exceeded*", - failureType = FailureType.TRANSIENT, - externalMessage = - "The query took too long to return results, the database read was aborted. Will retry.", - sampleInternalMessage = - "java.lang.RuntimeException: java.lang.RuntimeException: java.sql.SQLException: " + - "Query execution was interrupted, maximum statement execution time exceeded", - referenceLinks = - listOf("https://github.com/airbytehq/airbyte-internal-issues/issues/7155"), - ), - ) - } -} diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcInitialSnapshotStateValue.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcInitialSnapshotStateValue.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcInitialSnapshotStateValue.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcInitialSnapshotStateValue.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcMetaFields.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcMetaFields.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcMetaFields.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcMetaFields.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcEncryption.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartition.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartition.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartition.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartition.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactory.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactory.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactory.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactory.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcStreamStateValue.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcStreamStateValue.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcStreamStateValue.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcStreamStateValue.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSource.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSource.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSource.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSource.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfiguration.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecification.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecification.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecification.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecification.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceMetadataQuerier.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceMetadataQuerier.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceMetadataQuerier.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceMetadataQuerier.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceOperations.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceOperations.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceOperations.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceOperations.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumOperations.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumOperations.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumOperations.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlDebeziumOperations.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlPosition.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlPosition.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlPosition.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/MySqlPosition.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLBooleanConverter.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLBooleanConverter.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLBooleanConverter.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLBooleanConverter.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLDateTimeConverter.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLDateTimeConverter.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLDateTimeConverter.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLDateTimeConverter.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLNumbericConverter.kt b/airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLNumbericConverter.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLNumbericConverter.kt rename to airbyte-integrations/connectors/source-mysql/src/main/kotlin/io/airbyte/integrations/source/mysql/cdc/converters/MySQLNumbericConverter.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/main/resources/application.yml b/airbyte-integrations/connectors/source-mysql/src/main/resources/application.yml similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/main/resources/application.yml rename to airbyte-integrations/connectors/source-mysql/src/main/resources/application.yml diff --git a/airbyte-integrations/connectors/source-mysql/src/main/resources/internal_models/internal_models.yaml b/airbyte-integrations/connectors/source-mysql/src/main/resources/internal_models/internal_models.yaml deleted file mode 100644 index d7c998e4c714..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/resources/internal_models/internal_models.yaml +++ /dev/null @@ -1,48 +0,0 @@ ---- -"$schema": http://json-schema.org/draft-07/schema# -title: MySQL Models -type: object -description: MySQL Models -properties: - state_type: - "$ref": "#/definitions/StateType" - primary_key_state: - "$ref": "#/definitions/PrimaryKeyLoadStatus" - cursor_based_state: - "$ref": "#/definitions/CursorBasedStatus" -definitions: - StateType: - description: Enum to define the sync mode of state. - type: string - enum: - - cursor_based - - primary_key - CursorBasedStatus: - type: object - extends: - type: object - existingJavaType: "io.airbyte.cdk.integrations.source.relationaldb.models.DbStreamState" - properties: - state_type: - "$ref": "#/definitions/StateType" - version: - description: Version of state. - type: integer - PrimaryKeyLoadStatus: - type: object - properties: - version: - description: Version of state. - type: integer - state_type: - "$ref": "#/definitions/StateType" - pk_name: - description: primary key name - type: string - pk_val: - description: primary key watermark - type: string - incremental_state: - description: State to switch to after completion of primary key initial sync - type: object - existingJavaType: com.fasterxml.jackson.databind.JsonNode diff --git a/airbyte-integrations/connectors/source-mysql/src/main/resources/spec.json b/airbyte-integrations/connectors/source-mysql/src/main/resources/spec.json deleted file mode 100644 index 5a9304326cdd..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/main/resources/spec.json +++ /dev/null @@ -1,252 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/mysql", - "connectionSpecification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MySql Source Spec", - "type": "object", - "required": ["host", "port", "database", "username", "replication_method"], - "properties": { - "host": { - "description": "The host name of the database.", - "title": "Host", - "type": "string", - "order": 0 - }, - "port": { - "description": "The port to connect to.", - "title": "Port", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 3306, - "examples": ["3306"], - "order": 1 - }, - "database": { - "description": "The database name.", - "title": "Database", - "type": "string", - "order": 2 - }, - "username": { - "description": "The username which is used to access the database.", - "title": "Username", - "type": "string", - "order": 3 - }, - "password": { - "description": "The password associated with the username.", - "title": "Password", - "type": "string", - "airbyte_secret": true, - "order": 4, - "always_show": true - }, - "jdbc_url_params": { - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). For more information read about JDBC URL parameters.", - "title": "JDBC URL Parameters (Advanced)", - "type": "string", - "order": 5 - }, - "ssl": { - "title": "SSL Connection", - "description": "Encrypt data using SSL.", - "type": "boolean", - "default": true, - "order": 6 - }, - "ssl_mode": { - "title": "SSL modes", - "description": "SSL connection modes. Read more in the docs.", - "type": "object", - "order": 7, - "oneOf": [ - { - "title": "preferred", - "description": "Automatically attempt SSL connection. If the MySQL server does not support SSL, continue with a regular connection.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "preferred", - "order": 0 - } - } - }, - { - "title": "required", - "description": "Always connect with SSL. If the MySQL server doesn’t support SSL, the connection will not be established. Certificate Authority (CA) and Hostname are not verified.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "required", - "order": 0 - } - } - }, - { - "title": "Verify CA", - "description": "Always connect with SSL. Verifies CA, but allows connection even if Hostname does not match.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_ca", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - }, - { - "title": "Verify Identity", - "description": "Always connect with SSL. Verify both CA and Hostname.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_identity", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - } - ] - }, - "replication_method": { - "type": "object", - "title": "Update Method", - "description": "Configures how data is extracted from the database.", - "order": 8, - "default": "CDC", - "display_type": "radio", - "oneOf": [ - { - "title": "Read Changes using Binary Log (CDC)", - "description": "Recommended - Incrementally reads new inserts, updates, and deletes using the MySQL binary log. This must be enabled on your database.", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "CDC", - "order": 0 - }, - "initial_waiting_seconds": { - "type": "integer", - "title": "Initial Waiting Time in Seconds (Advanced)", - "description": "The amount of time the connector will wait when it launches to determine if there is new data to sync or not. Defaults to 300 seconds. Valid range: 120 seconds to 1200 seconds. Read about initial waiting time.", - "default": 300, - "min": 120, - "max": 1200, - "order": 1, - "always_show": true - }, - "server_time_zone": { - "type": "string", - "title": "Configured server timezone for the MySQL source (Advanced)", - "description": "Enter the configured MySQL server timezone. This should only be done if the configured timezone in your MySQL instance does not conform to IANNA standard.", - "order": 2, - "always_show": true - }, - "invalid_cdc_cursor_position_behavior": { - "type": "string", - "title": "Invalid CDC position behavior (Advanced)", - "description": "Determines whether Airbyte should fail or re-sync data in case of an stale/invalid cursor value into the WAL. If 'Fail sync' is chosen, a user will have to manually reset the connection before being able to continue syncing data. If 'Re-sync data' is chosen, Airbyte will automatically trigger a refresh but could lead to higher cloud costs and data loss.", - "enum": ["Fail sync", "Re-sync data"], - "default": "Fail sync", - "order": 3, - "always_show": true - }, - "initial_load_timeout_hours": { - "type": "integer", - "title": "Initial Load Timeout in Hours (Advanced)", - "description": "The amount of time an initial load is allowed to continue for before catching up on CDC logs.", - "default": 8, - "min": 4, - "max": 24, - "order": 4, - "always_show": true - } - } - }, - { - "title": "Scan Changes with User Defined Cursor", - "description": "Incrementally detects new inserts and updates using the cursor column chosen when configuring a connection (e.g. created_at, updated_at).", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "STANDARD", - "order": 0 - } - } - } - ] - } - } - } -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractMySqlSourceDatatypeTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractMySqlSourceDatatypeTest.java deleted file mode 100644 index 8eb622e72716..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractMySqlSourceDatatypeTest.java +++ /dev/null @@ -1,501 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.integrations.standardtest.source.AbstractSourceDatabaseTypeTest; -import io.airbyte.cdk.integrations.standardtest.source.TestDataHolder; -import io.airbyte.cdk.integrations.standardtest.source.TestDestinationEnv; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.protocol.models.JsonSchemaType; -import java.io.File; -import java.io.IOException; -import java.util.Base64; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public abstract class AbstractMySqlSourceDatatypeTest extends AbstractSourceDatabaseTypeTest { - - protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractMySqlSourceDatatypeTest.class); - - protected MySQLTestDatabase testdb; - - @Override - protected String getNameSpace() { - return testdb.getDatabaseName(); - } - - @Override - protected void tearDown(final TestDestinationEnv testEnv) { - testdb.close(); - } - - @Override - protected String getImageName() { - return "airbyte/source-mysql:dev"; - } - - @Override - protected void initTests() { - // bit defaults to bit(1), which is equivalent to boolean - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("bit") - .airbyteType(JsonSchemaType.BOOLEAN) - .addInsertValues("null", "1", "0") - .addExpectedValues(null, "true", "false") - .build()); - - // bit(1) is equivalent to boolean - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("bit") - .fullSourceDataType("bit(1)") - .airbyteType(JsonSchemaType.BOOLEAN) - .addInsertValues("null", "1", "0") - .addExpectedValues(null, "true", "false") - .build()); - - // bit(>1) is binary - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("bit") - .fullSourceDataType("bit(7)") - .airbyteType(JsonSchemaType.STRING_BASE_64) - // 1000001 is binary for A - .addInsertValues("null", "b'1000001'") - // QQo= is base64 encoding in charset UTF-8 for A - .addExpectedValues(null, "QQ==") - .build()); - - // tinyint without width - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("tinyint") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "-128", "127") - .addExpectedValues(null, "-128", "127") - .build()); - - // tinyint(1) is equivalent to boolean - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("tinyint") - .fullSourceDataType("tinyint(1)") - .airbyteType(JsonSchemaType.BOOLEAN) - .addInsertValues("null", "1", "0") - .addExpectedValues(null, "true", "false") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("tinyint") - .fullSourceDataType("tinyint(1) unsigned") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "0", "1", "2", "3") - .addExpectedValues(null, "0", "1", "2", "3") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("tinyint") - .fullSourceDataType("tinyint(2)") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "-128", "127") - .addExpectedValues(null, "-128", "127") - .build()); - - final Set booleanTypes = Set.of("BOOLEAN", "BOOL"); - for (final String booleanType : booleanTypes) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(booleanType) - .airbyteType(JsonSchemaType.BOOLEAN) - // MySql booleans are tinyint(1), and only 1 is true - .addInsertValues("null", "1", "0") - .addExpectedValues(null, "true", "false") - .build()); - } - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("smallint") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "-32768", "32767") - .addExpectedValues(null, "-32768", "32767") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("smallint") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("smallint zerofill") - .addInsertValues("1") - .addExpectedValues("1") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("smallint") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("smallint unsigned") - .addInsertValues("null", "0", "65535") - .addExpectedValues(null, "0", "65535") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("mediumint") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "-8388608", "8388607") - .addExpectedValues(null, "-8388608", "8388607") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("mediumint") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("mediumint zerofill") - .addInsertValues("1") - .addExpectedValues("1") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("int") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "-2147483648", "2147483647") - .addExpectedValues(null, "-2147483648", "2147483647") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("int") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("int unsigned") - .addInsertValues("3428724653") - .addExpectedValues("3428724653") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("int") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("int zerofill") - .addInsertValues("1") - .addExpectedValues("1") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("bigint") - .airbyteType(JsonSchemaType.INTEGER) - .addInsertValues("null", "9223372036854775807") - .addExpectedValues(null, "9223372036854775807") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("float") - .airbyteType(JsonSchemaType.NUMBER) - .addInsertValues("null", "10.5") - .addExpectedValues(null, "10.5") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("double") - .airbyteType(JsonSchemaType.NUMBER) - .addInsertValues("null", "power(10, 308)", "1/power(10, 45)", "10.5") - .addExpectedValues(null, String.valueOf(Math.pow(10, 308)), String.valueOf(1 / Math.pow(10, 45)), "10.5") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("decimal") - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("decimal(10,3)") - .addInsertValues("0.188", "null") - .addExpectedValues("0.188", null) - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("decimal") - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("decimal(32,0)") - .addInsertValues("1700000.01", "123") - .addExpectedValues("1700000", "123") - .build()); - - for (final String type : Set.of("date", "date not null default '0000-00-00'")) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("date") - .fullSourceDataType(type) - .airbyteType(JsonSchemaType.STRING_DATE) - .addInsertValues("'1999-01-08'", "'2021-01-01'", "'2022/11/12'", "'1987.12.01'") - .addExpectedValues("1999-01-08", "2021-01-01", "2022-11-12", "1987-12-01") - .build()); - } - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("date") - .airbyteType(JsonSchemaType.STRING_DATE) - .addInsertValues("null") - .addExpectedValues((String) null) - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("date") - .airbyteType(JsonSchemaType.STRING_DATE) - .addInsertValues("0000-00-00") - .addExpectedValues((String) null) - .build()); - - for (final String fullSourceType : Set.of("datetime", "datetime not null default now()")) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("datetime") - .fullSourceDataType(fullSourceType) - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITHOUT_TIMEZONE) - .addInsertValues("'2005-10-10 23:22:21'", "'2013-09-05T10:10:02'", "'2013-09-06T10:10:02'") - .addExpectedValues("2005-10-10T23:22:21", "2013-09-05T10:10:02", "2013-09-06T10:10:02") - .build()); - } - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("datetime") - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITHOUT_TIMEZONE) - .addInsertValues("null") - .addExpectedValues((String) null) - .build()); - - addTimestampDataTypeTest(); - - for (final String fullSourceType : Set.of("time", "time not null default '00:00:00'")) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("time") - .fullSourceDataType(fullSourceType) - .airbyteType(JsonSchemaType.STRING_TIME_WITHOUT_TIMEZONE) - // JDBC driver can process only "clock"(00:00:00-23:59:59) values. - .addInsertValues("'-22:59:59'", "'23:59:59'", "'00:00:00'") - .addExpectedValues("22:59:59", "23:59:59", "00:00:00.000000") - .build()); - - } - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("time") - .airbyteType(JsonSchemaType.STRING_TIME_WITHOUT_TIMEZONE) - // JDBC driver can process only "clock"(00:00:00-23:59:59) values. - .addInsertValues("null") - .addExpectedValues((String) null) - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("year") - .airbyteType(JsonSchemaType.INTEGER) - // MySQL converts values in the ranges '0' - '69' to YEAR value in the range 2000 - 2069 - // and '70' - '99' to 1970 - 1999. - .addInsertValues("null", "'1997'", "'0'", "'50'", "'70'", "'80'", "'99'", "'00'", "'000'") - .addExpectedValues(null, "1997", "2000", "2050", "1970", "1980", "1999", "2000", "2000") - .build()); - - // char types can be string or binary, so they are tested separately - final Set charTypes = Stream.of(MysqlType.CHAR, MysqlType.VARCHAR) - .map(Enum::name) - .collect(Collectors.toSet()); - for (final String charType : charTypes) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(charType) - .airbyteType(JsonSchemaType.STRING) - .fullSourceDataType(charType + "(63)") - .addInsertValues("null", "'Airbyte'", "'!\"#$%&\\'()*+,-./:;<=>?\\@[\\]^_\\`{|}~'") - .addExpectedValues(null, "Airbyte", "!\"#$%&'()*+,-./:;<=>?@[]^_`{|}~") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(charType) - .airbyteType(JsonSchemaType.STRING) - .fullSourceDataType(charType + "(63) character set utf16") - .addInsertValues("0xfffd") - .addExpectedValues("�") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(charType) - .airbyteType(JsonSchemaType.STRING) - .fullSourceDataType(charType + "(63) character set cp1251") - .addInsertValues("'тест'") - .addExpectedValues("тест") - .build()); - - // when charset is binary, return binary in base64 encoding in charset UTF-8 - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(charType) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType(charType + "(7) character set binary") - .addInsertValues("null", "'Airbyte'") - .addExpectedValues(null, "QWlyYnl0ZQ==") - .build()); - } - - final Set blobTypes = Stream - .of(MysqlType.TINYBLOB, MysqlType.BLOB, MysqlType.MEDIUMBLOB, MysqlType.LONGBLOB) - .map(Enum::name) - .collect(Collectors.toSet()); - for (final String blobType : blobTypes) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(blobType) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .addInsertValues("null", "'Airbyte'") - .addExpectedValues(null, "QWlyYnl0ZQ==") - .build()); - } - - // binary appends '\0' to the end of the string - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(MysqlType.BINARY.name()) - .fullSourceDataType(MysqlType.BINARY.name() + "(10)") - .airbyteType(JsonSchemaType.STRING_BASE_64) - .addInsertValues("null", "'Airbyte'") - .addExpectedValues(null, "QWlyYnl0ZQAAAA==") - .build()); - - // varbinary does not append '\0' to the end of the string - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(MysqlType.VARBINARY.name()) - .fullSourceDataType(MysqlType.VARBINARY.name() + "(10)") - .airbyteType(JsonSchemaType.STRING_BASE_64) - .addInsertValues("null", "'Airbyte'") - .addExpectedValues(null, "QWlyYnl0ZQ==") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(MysqlType.VARBINARY.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType(MysqlType.VARBINARY.name() + "(20000)") // size should be enough to save test.png - .addInsertValues("null", "'test'", "'тест'", String.format("FROM_BASE64('%s')", getFileDataInBase64())) - .addExpectedValues(null, "dGVzdA==", "0YLQtdGB0YI=", getFileDataInBase64()) - .build()); - - final Set textTypes = Stream - .of(MysqlType.TINYTEXT, MysqlType.TEXT, MysqlType.MEDIUMTEXT, MysqlType.LONGTEXT) - .map(Enum::name) - .collect(Collectors.toSet()); - final String randomText = RandomStringUtils.random(50, true, true); - for (final String textType : textTypes) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(textType) - .airbyteType(JsonSchemaType.STRING) - .addInsertValues("null", "'Airbyte'", String.format("'%s'", randomText)) - .addExpectedValues(null, "Airbyte", randomText) - .build()); - } - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("mediumtext") - .airbyteType(JsonSchemaType.STRING) - .addInsertValues(getLogString(1048000), "'test'") - .addExpectedValues(StringUtils.leftPad("0", 1048000, "0"), "test") - .build()); - - addJsonDataTypeTest(); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("enum") - .fullSourceDataType("ENUM('xs', 's', 'm', 'l', 'xl')") - .airbyteType(JsonSchemaType.STRING) - .addInsertValues("null", "'xs'", "'m'") - .addExpectedValues(null, "xs", "m") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("set") - .fullSourceDataType("SET('xs', 's', 'm', 'l', 'xl')") - .airbyteType(JsonSchemaType.STRING) - .addInsertValues("null", "'xs,s'", "'m,xl'") - .addExpectedValues(null, "xs,s", "m,xl") - .build()); - - addDecimalValuesTest(); - } - - protected void addJsonDataTypeTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("json") - .airbyteType(JsonSchemaType.STRING) - .addInsertValues("null", "'{\"a\": 10, \"b\": 15}'", "'{\"fóo\": \"bär\"}'", "'{\"春江潮水连海平\": \"海上明月共潮生\"}'") - .addExpectedValues(null, "{\"a\": 10, \"b\": 15}", "{\"fóo\": \"bär\"}", "{\"春江潮水连海平\": \"海上明月共潮生\"}") - .build()); - } - - protected void addTimestampDataTypeTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("timestamp") - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITH_TIMEZONE) - .addInsertValues("null", "'2021-01-00'", "'2021-00-00'", "'0000-00-00'", "'2022-08-09T10:17:16.161342Z'") - .addExpectedValues(null, null, null, null, "2022-08-09T10:17:16.000000Z") - .build()); - } - - private String getLogString(final int length) { - final int maxLpadLength = 262144; - final StringBuilder stringBuilder = new StringBuilder("concat("); - final int fullChunks = length / maxLpadLength; - stringBuilder.append("lpad('0', 262144, '0'),".repeat(fullChunks)); - stringBuilder.append("lpad('0', ").append(length % maxLpadLength).append(", '0'))"); - return stringBuilder.toString(); - } - - private String getFileDataInBase64() { - final File file = new File(getClass().getClassLoader().getResource("test.png").getFile()); - try { - return Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(file)); - } catch (final IOException e) { - LOGGER.error(String.format("Fail to read the file: %s. Error: %s", file.getAbsoluteFile(), e.getMessage())); - } - return null; - } - - protected void addDecimalValuesTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("decimal") - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("decimal(19,2)") - .addInsertValues("1700000.01", "'123'") - .addExpectedValues("1700000.01", "123.0") - .build()); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractSshMySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractSshMySqlSourceAcceptanceTest.java deleted file mode 100644 index 765495b85469..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/AbstractSshMySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Lists; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.cdk.integrations.standardtest.source.SourceAcceptanceTest; -import io.airbyte.cdk.integrations.standardtest.source.TestDestinationEnv; -import io.airbyte.commons.io.IOs; -import io.airbyte.commons.json.Jsons; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import io.airbyte.protocol.models.v0.DestinationSyncMode; -import io.airbyte.protocol.models.v0.SyncMode; -import java.nio.file.Path; -import java.util.HashMap; - -public abstract class AbstractSshMySqlSourceAcceptanceTest extends SourceAcceptanceTest { - - private static final String STREAM_NAME = "id_and_name"; - private static final String STREAM_NAME2 = "starships"; - - private JsonNode config; - - public abstract Path getConfigFilePath(); - - @Override - protected void setupEnvironment(final TestDestinationEnv environment) { - config = Jsons.deserialize(IOs.readFile(getConfigFilePath())); - } - - @Override - protected void tearDown(final TestDestinationEnv testEnv) {} - - @Override - protected String getImageName() { - return "airbyte/source-mysql:dev"; - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.getSpecAndInjectSsh(); - } - - @Override - protected JsonNode getConfig() { - return config; - } - - @Override - protected ConfiguredAirbyteCatalog getConfiguredCatalog() { - return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList( - new ConfiguredAirbyteStream() - .withSyncMode(SyncMode.INCREMENTAL) - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - String.format("%s", STREAM_NAME), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL))), - new ConfiguredAirbyteStream() - .withSyncMode(SyncMode.INCREMENTAL) - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - String.format("%s", STREAM_NAME2), - config.get(JdbcUtils.DATABASE_KEY).asText(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL))))); - } - - @Override - protected JsonNode getState() { - return Jsons.jsonNode(new HashMap<>()); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CDCMySqlDatatypeAccuracyTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CDCMySqlDatatypeAccuracyTest.java deleted file mode 100644 index 9400c4e66368..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CDCMySqlDatatypeAccuracyTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.Database; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import org.junit.jupiter.api.Test; - -public class CDCMySqlDatatypeAccuracyTest extends MySqlDatatypeAccuracyTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withoutSsl() - .withCdcReplication() - .with("snapshot_mode", "initial_only") - .build(); - } - - @Override - protected Database setupDatabase() { - testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8).withoutStrictMode().withCdcPermissions(); - return testdb.getDatabase(); - } - - // Temporarily disable this test since it's causing trouble on GHA. - @Override - @Test - public void testDataContent() { - // Do Nothing - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcBinlogsMySqlSourceDatatypeTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcBinlogsMySqlSourceDatatypeTest.java deleted file mode 100644 index 82ab112d7e15..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcBinlogsMySqlSourceDatatypeTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.Iterables; -import io.airbyte.cdk.db.Database; -import io.airbyte.cdk.integrations.standardtest.source.TestDataHolder; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import java.util.List; - -public class CdcBinlogsMySqlSourceDatatypeTest extends AbstractMySqlSourceDatatypeTest { - - private JsonNode stateAfterFirstSync; - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withoutSsl() - .withCdcReplication() - .build(); - } - - @Override - protected Database setupDatabase() { - testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8).withoutStrictMode().withCdcPermissions(); - return testdb.getDatabase(); - } - - @Override - protected List runRead(final ConfiguredAirbyteCatalog configuredCatalog) throws Exception { - if (stateAfterFirstSync == null) { - throw new RuntimeException("stateAfterFirstSync is null"); - } - return super.runRead(configuredCatalog, stateAfterFirstSync); - } - - @Override - protected void postSetup() throws Exception { - final var database = testdb.getDatabase(); - for (final TestDataHolder test : testDataHolders) { - database.query(ctx -> { - ctx.execute("TRUNCATE TABLE " + test.getNameWithTestPrefix() + ";"); - return null; - }); - } - - final ConfiguredAirbyteStream dummyTableWithData = createDummyTableWithData(database); - final ConfiguredAirbyteCatalog catalog = getConfiguredCatalog(); - catalog.getStreams().add(dummyTableWithData); - - final List allMessages = super.runRead(catalog); - final List stateAfterFirstBatch = extractStateMessages(allMessages); - stateAfterFirstSync = Jsons.jsonNode(List.of(Iterables.getLast(stateAfterFirstBatch))); - if (stateAfterFirstSync == null) { - throw new RuntimeException("stateAfterFirstSync should not be null"); - } - for (final TestDataHolder test : testDataHolders) { - database.query(ctx -> { - test.getInsertSqlQueries().forEach(ctx::fetch); - return null; - }); - } - } - - @Override - public boolean testCatalog() { - return true; - } - - @Override - protected void addTimestampDataTypeTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("timestamp") - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITH_TIMEZONE) - .addInsertValues("null", "'2021-01-00'", "'2021-00-00'", "'0000-00-00'", "'2022-08-09T10:17:16.161342Z'") - .addExpectedValues(null, "1970-01-01T00:00:00.000000Z", "1970-01-01T00:00:00.000000Z", "1970-01-01T00:00:00.000000Z", - "2022-08-09T10:17:16.000000Z") - .build()); - } - - @Override - protected void addJsonDataTypeTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("json") - .airbyteType(JsonSchemaType.STRING) - .addInsertValues("null", "'{\"a\":10,\"b\":15}'", "'{\"fóo\":\"bär\"}'", "'{\"春江潮水连海平\":\"海上明月共潮生\"}'") - .addExpectedValues(null, "{\"a\":10,\"b\":15}", "{\"fóo\":\"bär\"}", "{\"春江潮水连海平\":\"海上明月共潮生\"}") - .build()); - } - - @Override - protected void addDecimalValuesTest() { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType("decimal") - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("decimal(19,2)") - .addInsertValues("1700000.01", "'123'") - .addExpectedValues("1700000.01", "123.00") - .build()); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcInitialSnapshotMySqlSourceDatatypeTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcInitialSnapshotMySqlSourceDatatypeTest.java deleted file mode 100644 index 6b971c86927c..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcInitialSnapshotMySqlSourceDatatypeTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.Database; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; - -public class CdcInitialSnapshotMySqlSourceDatatypeTest extends AbstractMySqlSourceDatatypeTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withoutSsl() - .withCdcReplication() - .with("snapshot_mode", "initial_only") - .build(); - } - - @Override - protected Database setupDatabase() { - testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8).withoutStrictMode().withCdcPermissions(); - return testdb.getDatabase(); - } - - @Override - public boolean testCatalog() { - return true; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSourceAcceptanceTest.java deleted file mode 100644 index 8286c3087991..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import static io.airbyte.protocol.models.v0.SyncMode.FULL_REFRESH; -import static io.airbyte.protocol.models.v0.SyncMode.INCREMENTAL; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.cdk.integrations.standardtest.source.SourceAcceptanceTest; -import io.airbyte.cdk.integrations.standardtest.source.TestDestinationEnv; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.AirbyteRecordMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import io.airbyte.protocol.models.v0.DestinationSyncMode; -import io.airbyte.protocol.models.v0.SyncMode; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import org.apache.commons.lang3.ArrayUtils; -import org.junit.Assert; -import org.junit.jupiter.api.Test; - -public class CdcMySqlSourceAcceptanceTest extends SourceAcceptanceTest { - - protected static final String STREAM_NAME = "id_and_name"; - protected static final String STREAM_NAME2 = "starships"; - protected static final String STREAM_NAME3 = "stream3"; - - protected MySQLTestDatabase testdb; - - @Override - protected String getImageName() { - return "airbyte/source-mysql:dev"; - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.getSpecAndInjectSsh(); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withCdcReplication() - .withoutSsl() - .build(); - } - - @Override - protected ConfiguredAirbyteCatalog getConfiguredCatalog() { - return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList( - new ConfiguredAirbyteStream() - .withSyncMode(INCREMENTAL) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME, testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSourceDefinedCursor(true) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, INCREMENTAL))), - new ConfiguredAirbyteStream() - .withSyncMode(INCREMENTAL) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME2, testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSourceDefinedCursor(true) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, INCREMENTAL))))); - } - - @Override - protected JsonNode getState() { - return null; - } - - @Override - protected void setupEnvironment(final TestDestinationEnv environment) { - testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8, getContainerModifiers()) - .withCdcPermissions() - .with("CREATE TABLE id_and_name(id INTEGER, name VARCHAR(200));") - .with("INSERT INTO id_and_name (id, name) VALUES (1,'picard'), (2, 'crusher'), (3, 'vash');") - .with("CREATE TABLE starships(id INTEGER, name VARCHAR(200));") - .with("INSERT INTO starships (id, name) VALUES (1,'enterprise-d'), (2, 'defiant'), (3, 'yamato');") - .with("CREATE TABLE %s (id INTEGER PRIMARY KEY, name VARCHAR(200), userid INTEGER DEFAULT NULL);", STREAM_NAME3) - .with("INSERT INTO %s (id, name) VALUES (4,'voyager');", STREAM_NAME3); - } - - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(); - } - - @Override - protected void tearDown(final TestDestinationEnv testEnv) { - testdb.close(); - } - - @Test - public void testIncrementalSyncShouldNotFailIfBinlogIsDeleted() throws Exception { - final ConfiguredAirbyteCatalog configuredCatalog = withSourceDefinedCursors(getConfiguredCatalog()); - // only sync incremental streams - configuredCatalog.setStreams( - configuredCatalog.getStreams().stream().filter(s -> s.getSyncMode() == INCREMENTAL).collect(Collectors.toList())); - - final List airbyteMessages = runRead(configuredCatalog, getState()); - final List recordMessages = filterRecords(airbyteMessages); - final List stateMessages = airbyteMessages - .stream() - .filter(m -> m.getType() == AirbyteMessage.Type.STATE) - .map(AirbyteMessage::getState) - .collect(Collectors.toList()); - assertFalse(recordMessages.isEmpty(), "Expected the first incremental sync to produce records"); - assertFalse(stateMessages.isEmpty(), "Expected incremental sync to produce STATE messages"); - - // when we run incremental sync again there should be no new records. Run a sync with the latest - // state message and assert no records were emitted. - final JsonNode latestState = Jsons.jsonNode(List.of(Iterables.getLast(stateMessages))); - // RESET MASTER removes all binary log files that are listed in the index file, - // leaving only a single, empty binary log file with a numeric suffix of .000001 - testdb.with("RESET MASTER;"); - - assertEquals(6, filterRecords(runRead(configuredCatalog, latestState)).size()); - } - - @Test - public void testIncrementalReadSelectedColumns() throws Exception { - final ConfiguredAirbyteCatalog catalog = getConfiguredCatalogWithPartialColumns(); - final List allMessages = runRead(catalog); - - final List records = filterRecords(allMessages); - assertFalse(records.isEmpty(), "Expected a incremental sync to produce records"); - verifyFieldNotExist(records, STREAM_NAME, "name"); - verifyFieldNotExist(records, STREAM_NAME2, "name"); - } - - private ConfiguredAirbyteCatalog getConfiguredCatalogWithPartialColumns() { - // We cannot strip the primary key field as that is required for a successful CDC sync - return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList( - new ConfiguredAirbyteStream() - .withSyncMode(INCREMENTAL) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME, testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER) - /* no name field */) - .withSourceDefinedCursor(true) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, INCREMENTAL))), - new ConfiguredAirbyteStream() - .withSyncMode(INCREMENTAL) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME2, testdb.getDatabaseName(), - /* no name field */ - Field.of("id", JsonSchemaType.NUMBER)) - .withSourceDefinedCursor(true) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, INCREMENTAL))))); - } - - private void verifyFieldNotExist(final List records, final String stream, final String field) { - assertTrue(records.stream().noneMatch(r -> r.getStream().equals(stream) && r.getData().get(field) != null), - "Records contain unselected columns [%s:%s]".formatted(stream, field)); - } - - @Test - protected void testNullValueConversion() throws Exception { - final List configuredAirbyteStreams = - Lists.newArrayList(new ConfiguredAirbyteStream() - .withSyncMode(INCREMENTAL) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream(STREAM_NAME3, - testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING), - Field.of("userid", JsonSchemaType.NUMBER)) - .withSourceDefinedCursor(true) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))) - .withSupportedSyncModes(Lists.newArrayList(FULL_REFRESH, INCREMENTAL)))); - - final ConfiguredAirbyteCatalog configuredCatalogWithOneStream = - new ConfiguredAirbyteCatalog().withStreams(List.of(configuredAirbyteStreams.get(0))); - - final List airbyteMessages = runRead(configuredCatalogWithOneStream, getState()); - final List recordMessages = filterRecords(airbyteMessages); - final List stateMessages = airbyteMessages - .stream() - .filter(m -> m.getType() == AirbyteMessage.Type.STATE) - .map(AirbyteMessage::getState) - .collect(Collectors.toList()); - Assert.assertEquals(recordMessages.size(), 1); - assertFalse(stateMessages.isEmpty(), "Reason"); - ObjectMapper mapper = new ObjectMapper(); - - assertEquals(cdcFieldsOmitted(recordMessages.get(0).getData()), - mapper.readTree("{\"id\":4, \"name\":\"voyager\", \"userid\":null}")); - - // when we run incremental sync again there should be no new records. Run a sync with the latest - // state message and assert no records were emitted. - JsonNode latestState = extractLatestState(stateMessages); - - testdb.getDatabase().query(c -> { - return c.query("INSERT INTO %s.%s (id, name) VALUES (5,'deep space nine');".formatted(testdb.getDatabaseName(), STREAM_NAME3)); - }).execute(); - - assert Objects.nonNull(latestState); - final List secondSyncRecords = filterRecords(runRead(configuredCatalogWithOneStream, latestState)); - assertFalse( - secondSyncRecords.isEmpty(), - "Expected the second incremental sync to produce records."); - assertEquals(cdcFieldsOmitted(secondSyncRecords.get(0).getData()), - mapper.readTree("{\"id\":5, \"name\":\"deep space nine\", \"userid\":null}")); - - } - - private JsonNode cdcFieldsOmitted(final JsonNode node) { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode object = mapper.createObjectNode(); - node.fieldNames().forEachRemaining(name -> { - if (!name.toLowerCase().startsWith("_ab_cdc_")) { - object.put(name, node.get(name)); - } - - }); - return object; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslCaCertificateSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslCaCertificateSourceAcceptanceTest.java deleted file mode 100644 index 98ccf8c7f50f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslCaCertificateSourceAcceptanceTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import org.apache.commons.lang3.ArrayUtils; - -public class CdcMySqlSslCaCertificateSourceAcceptanceTest extends CdcMySqlSourceAcceptanceTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withCdcReplication() - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCertificates().caCertificate()) - .build()) - .build(); - } - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslRequiredSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslRequiredSourceAcceptanceTest.java deleted file mode 100644 index f508513b72d8..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CdcMySqlSslRequiredSourceAcceptanceTest.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.standardtest.source.TestDestinationEnv; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import org.apache.commons.lang3.ArrayUtils; - -public class CdcMySqlSslRequiredSourceAcceptanceTest extends CdcMySqlSourceAcceptanceTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withCdcReplication() - .withSsl(ImmutableMap.builder().put(JdbcUtils.MODE_KEY, "required").build()) - .build(); - } - - @Override - protected void setupEnvironment(final TestDestinationEnv environment) { - super.setupEnvironment(environment); - testdb.with("ALTER USER %s REQUIRE SSL;", testdb.getUserName()); - } - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES, ContainerModifier.CLIENT_CERTITICATE); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSourceAcceptanceTest.java deleted file mode 100644 index 8b607cc092f8..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.commons.features.FeatureFlags; -import io.airbyte.commons.features.FeatureFlagsWrapper; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.resources.MoreResources; -import io.airbyte.protocol.models.v0.ConnectorSpecification; - -public class CloudDeploymentMySqlSourceAcceptanceTest extends MySqlSslSourceAcceptanceTest { - - @Override - protected FeatureFlags featureFlags() { - return FeatureFlagsWrapper.overridingDeploymentMode(super.featureFlags(), "CLOUD"); - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.injectSshIntoSpec(Jsons.deserialize(MoreResources.readResource("expected_cloud_spec.json"), ConnectorSpecification.class)); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslCaCertificateSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslCaCertificateSourceAcceptanceTest.java deleted file mode 100644 index 15f0b5b5a612..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslCaCertificateSourceAcceptanceTest.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.commons.features.FeatureFlags; -import io.airbyte.commons.features.FeatureFlagsWrapper; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.resources.MoreResources; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import org.apache.commons.lang3.ArrayUtils; - -public class CloudDeploymentMySqlSslCaCertificateSourceAcceptanceTest extends MySqlSourceAcceptanceTest { - - private static final String PASSWORD = "Passw0rd"; - - @Override - protected FeatureFlags featureFlags() { - return FeatureFlagsWrapper.overridingDeploymentMode(super.featureFlags(), "CLOUD"); - } - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCaCertificate()) - .put("client_key_password", PASSWORD) - .build()) - .build(); - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.injectSshIntoSpec(Jsons.deserialize(MoreResources.readResource("expected_cloud_spec.json"), ConnectorSpecification.class)); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslFullCertificateSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslFullCertificateSourceAcceptanceTest.java deleted file mode 100644 index 298276ee443f..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/CloudDeploymentMySqlSslFullCertificateSourceAcceptanceTest.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.commons.features.FeatureFlags; -import io.airbyte.commons.features.FeatureFlagsWrapper; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.resources.MoreResources; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import org.apache.commons.lang3.ArrayUtils; - -public class CloudDeploymentMySqlSslFullCertificateSourceAcceptanceTest extends MySqlSourceAcceptanceTest { - - private static final String PASSWORD = "Passw0rd"; - - @Override - protected FeatureFlags featureFlags() { - return FeatureFlagsWrapper.overridingDeploymentMode(super.featureFlags(), "CLOUD"); - } - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES, ContainerModifier.CLIENT_CERTITICATE); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCertificates().caCertificate()) - .put("client_certificate", testdb.getCertificates().clientCertificate()) - .put("client_key", testdb.getCertificates().clientKey()) - .put("client_key_password", PASSWORD) - .build()) - .build(); - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.injectSshIntoSpec(Jsons.deserialize(MoreResources.readResource("expected_cloud_spec.json"), ConnectorSpecification.class)); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlDatatypeAccuracyTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlDatatypeAccuracyTest.java deleted file mode 100644 index 516d6c20a425..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlDatatypeAccuracyTest.java +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.Database; -import io.airbyte.cdk.integrations.standardtest.source.TestDataHolder; -import io.airbyte.integrations.source.mysql.MySQLContainerFactory; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.protocol.models.JsonSchemaType; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -public class MySqlDatatypeAccuracyTest extends AbstractMySqlSourceDatatypeTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withoutSsl() - .withStandardReplication() - .build(); - } - - @Override - protected Database setupDatabase() { - final var sharedContainer = new MySQLContainerFactory().shared("mysql:8.0"); - testdb = new MySQLTestDatabase(sharedContainer) - .withConnectionProperty("zeroDateTimeBehavior", "convertToNull") - .initialized() - .withoutStrictMode(); - return testdb.getDatabase(); - } - - private final Map> charsetsCollationsMap = Map.of( - "UTF8", Arrays.asList("UTF8_bin", "UTF8_general_ci"), - "UTF8MB4", Arrays.asList("UTF8MB4_general_ci", "utf8mb4_0900_ai_ci"), - "UTF16", Arrays.asList("UTF16_bin", "UTF16_general_ci"), - "binary", Arrays.asList("binary"), - "CP1250", Arrays.asList("CP1250_general_ci", "cp1250_czech_cs")); - - @Override - public boolean testCatalog() { - return true; - } - - @Override - protected void initTests() { - for (final MysqlType mst : MysqlType.values()) { - switch (mst) { - case DECIMAL -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("%s(10,0)".formatted(mst.getName())) - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("%s(%d,30)".formatted(mst.getName(), mst.getPrecision())) - .build()); - } - case DECIMAL_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("DECIMAL(32,0) UNSIGNED") - .build()); - - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("DECIMAL(%d,30) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case TINYINT -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.BOOLEAN) - .fullSourceDataType("%s(1)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("%s(%d)".formatted(mst.getName(), mst.getPrecision())) - .build()); - } - case TINYINT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("TINYINT(1) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("TINYINT(%d) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case BOOLEAN -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.BOOLEAN) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case SMALLINT, BIGINT, MEDIUMINT, INT -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("%s(1)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("%s(%d)".formatted(mst.getName(), mst.getPrecision())) - .build()); - } - case SMALLINT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("SMALLINT(1) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("SMALLINT(%d) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case INT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("INT(1) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("INT(%d) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case FLOAT -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("%s(0)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("%s(24)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("%s(25)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("%s(53)".formatted(mst.getName())) - .build()); - } - case FLOAT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("FLOAT(0) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("FLOAT(24) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("FLOAT(25) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("FLOAT(53) UNSIGNED") - .build()); - - } - case DOUBLE -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("DOUBLE PRECISION") - .build()); - } - case DOUBLE_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.NUMBER) - .fullSourceDataType("DOUBLE PRECISION UNSIGNED") - .build()); - } - case TIMESTAMP -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITH_TIMEZONE) - .fullSourceDataType("%s(0)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITH_TIMEZONE) - .fullSourceDataType("%s(6)".formatted(mst.getName())) - .build()); - } - case BIGINT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("BIGINT(1) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("BIGINT(%d) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case MEDIUMINT_UNSIGNED -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("MEDIUMINT(1) UNSIGNED") - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("MEDIUMINT(%d) UNSIGNED".formatted(mst.getPrecision())) - .build()); - } - case DATE -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_DATE) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case TIME -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_TIME_WITHOUT_TIMEZONE) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case DATETIME -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_TIMESTAMP_WITHOUT_TIMEZONE) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case YEAR -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.INTEGER) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case VARCHAR -> { - for (final Entry entry : charsetsCollationsMap.entrySet()) { - List collations = (List) entry.getValue(); - final var airbyteType = (entry.getKey() == "binary") ? JsonSchemaType.STRING_BASE_64 : JsonSchemaType.STRING; - for (final String collation : collations) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(0) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(60000) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - } - } - } - case VARBINARY -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(1)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(65000)".formatted(mst.getName())) - .build()); - } - case BIT -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.BOOLEAN) - .fullSourceDataType("%s(1)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(64)".formatted(mst.getName())) - .build()); - - } - case JSON -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case ENUM, SET -> { - for (final Entry entry : charsetsCollationsMap.entrySet()) { - List collations = (List) entry.getValue(); - for (final String collation : collations) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING) - .fullSourceDataType( - "%s('value1', 'value2', 'value3') CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - } - } - } - case TINYBLOB, MEDIUMBLOB, LONGBLOB, GEOMETRY -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s".formatted(mst.getName())) - .build()); - } - case TINYTEXT, MEDIUMTEXT, LONGTEXT -> { - for (final Entry entry : charsetsCollationsMap.entrySet()) { - final var airbyteType = (entry.getKey() == "binary") ? JsonSchemaType.STRING_BASE_64 : JsonSchemaType.STRING; - List collations = (List) entry.getValue(); - for (final String collation : collations) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - } - } - } - case BLOB -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(0)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(65000)".formatted(mst.getName())) - .build()); - } - case TEXT -> { - for (final Entry entry : charsetsCollationsMap.entrySet()) { - final var airbyteType = (entry.getKey() == "binary") ? JsonSchemaType.STRING_BASE_64 : JsonSchemaType.STRING; - List collations = (List) entry.getValue(); - for (final String collation : collations) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(0) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(65000) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - } - } - } - case CHAR -> { - for (final Entry entry : charsetsCollationsMap.entrySet()) { - final var airbyteType = (entry.getKey() == "binary") ? JsonSchemaType.STRING_BASE_64 : JsonSchemaType.STRING; - List collations = (List) entry.getValue(); - for (final String collation : collations) { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(0) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(airbyteType) - .fullSourceDataType("%s(255) CHARACTER SET %s COLLATE %s".formatted(mst.getName(), entry.getKey(), collation)) - .build()); - } - } - } - case BINARY -> { - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(0)".formatted(mst.getName())) - .build()); - addDataTypeTestData( - TestDataHolder.builder() - .sourceType(mst.name()) - .airbyteType(JsonSchemaType.STRING_BASE_64) - .fullSourceDataType("%s(255)".formatted(mst.getName())) - .build()); - } - case NULL, UNKNOWN -> { - // no-op - } - default -> throw new IllegalStateException("Unexpected value: " + mst); - } - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceAcceptanceTest.java deleted file mode 100644 index 6044c66cf9cb..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import static io.airbyte.protocol.models.v0.SyncMode.INCREMENTAL; -import static org.junit.Assert.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.collect.Lists; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.cdk.integrations.standardtest.source.SourceAcceptanceTest; -import io.airbyte.cdk.integrations.standardtest.source.TestDestinationEnv; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.*; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import org.apache.commons.lang3.ArrayUtils; -import org.junit.jupiter.api.Test; - -public class MySqlSourceAcceptanceTest extends SourceAcceptanceTest { - - protected MySQLTestDatabase testdb; - - private static final String STREAM_NAME = "id_and_name"; - private static final String STREAM_NAME2 = "public.starships"; - - @Override - protected void setupEnvironment(final TestDestinationEnv environment) throws Exception { - testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8, getContainerModifiers()) - .with("CREATE TABLE id_and_name(id INTEGER, name VARCHAR(200));") - .with("INSERT INTO id_and_name (id, name) VALUES (1,'picard'), (2, 'crusher'), (3, 'vash');") - .with("CREATE TABLE starships(id INTEGER, name VARCHAR(200));") - .with("INSERT INTO starships (id, name) VALUES (1,'enterprise-d'), (2, 'defiant'), (3, 'yamato');"); - } - - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(); - } - - @Override - protected void tearDown(final TestDestinationEnv testEnv) { - testdb.close(); - } - - @Override - protected String getImageName() { - return "airbyte/source-mysql:dev"; - } - - @Override - protected ConnectorSpecification getSpec() throws Exception { - return SshHelpers.getSpecAndInjectSsh(); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withoutSsl() - .build(); - } - - @Override - protected ConfiguredAirbyteCatalog getConfiguredCatalog() { - return new ConfiguredAirbyteCatalog().withStreams(Lists.newArrayList( - new ConfiguredAirbyteStream() - .withSyncMode(SyncMode.INCREMENTAL) - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME, testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL))), - new ConfiguredAirbyteStream() - .withSyncMode(SyncMode.INCREMENTAL) - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withStream(CatalogHelpers.createAirbyteStream( - STREAM_NAME2, testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL))))); - } - - @Override - protected JsonNode getState() { - return Jsons.jsonNode(new HashMap<>()); - } - - @Test - protected void testNullValueConversion() throws Exception { - final String STREAM_NAME3 = "stream3"; - testdb.getDatabase().query(c -> { - return c.query(""" - CREATE TABLE %s.%s (id INTEGER PRIMARY KEY, name VARCHAR(200), userid INTEGER DEFAULT NULL); - """.formatted(testdb.getDatabaseName(), STREAM_NAME3)); - }).execute(); - - testdb.getDatabase().query(c -> { - return c.query(""" - INSERT INTO %s.%s (id, name) VALUES (4,'voyager'); - """.formatted(testdb.getDatabaseName(), STREAM_NAME3)); - }).execute(); - - final List configuredAirbyteStreams = - Lists.newArrayList(CatalogHelpers.createConfiguredAirbyteStream(STREAM_NAME3, - testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.NUMBER), - Field.of("name", JsonSchemaType.STRING), - Field.of("userid", JsonSchemaType.NUMBER)) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withSyncMode(INCREMENTAL) - .withCursorField(List.of("id"))); - final ConfiguredAirbyteCatalog configuredCatalogWithOneStream = - new ConfiguredAirbyteCatalog().withStreams(List.of(configuredAirbyteStreams.get(0))); - - final List airbyteMessages = runRead(configuredCatalogWithOneStream, getState()); - final List recordMessages = filterRecords(airbyteMessages); - final List stateMessages = airbyteMessages - .stream() - .filter(m -> m.getType() == AirbyteMessage.Type.STATE) - .map(AirbyteMessage::getState) - .collect(Collectors.toList()); - assertEquals(recordMessages.size(), 1); - assertFalse(stateMessages.isEmpty(), "Reason"); - ObjectMapper mapper = new ObjectMapper(); - - assertEquals(recordMessages.get(0).getData(), - mapper.readTree("{\"id\":4, \"name\":\"voyager\", \"userid\":null}")); - - // when we run incremental sync again there should be no new records. Run a sync with the latest - // state message and assert no records were emitted. - JsonNode latestState = extractLatestState(stateMessages); - - testdb.getDatabase().query(c -> { - return c.query("INSERT INTO %s.%s (id, name) VALUES (5,'deep space nine');".formatted(testdb.getDatabaseName(), STREAM_NAME3)); - }).execute(); - - assert Objects.nonNull(latestState); - final List secondSyncRecords = filterRecords(runRead(configuredCatalogWithOneStream, latestState)); - assertFalse( - secondSyncRecords.isEmpty(), - "Expected the second incremental sync to produce records."); - assertEquals(secondSyncRecords.get(0).getData(), - mapper.readTree("{\"id\":5, \"name\":\"deep space nine\", \"userid\":null}")); - - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceDatatypeTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceDatatypeTest.java deleted file mode 100644 index cbfa689562dc..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSourceDatatypeTest.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.Database; -import io.airbyte.integrations.source.mysql.MySQLContainerFactory; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; - -public class MySqlSourceDatatypeTest extends AbstractMySqlSourceDatatypeTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withoutSsl() - .withStandardReplication() - .build(); - } - - @Override - protected Database setupDatabase() { - final var sharedContainer = new MySQLContainerFactory().shared("mysql:8.0"); - testdb = new MySQLTestDatabase(sharedContainer) - .withConnectionProperty("zeroDateTimeBehavior", "convertToNull") - .initialized() - .withoutStrictMode(); - return testdb.getDatabase(); - } - - @Override - public boolean testCatalog() { - return true; - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslCaCertificateSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslCaCertificateSourceAcceptanceTest.java deleted file mode 100644 index 71f36aa027f4..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslCaCertificateSourceAcceptanceTest.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import org.apache.commons.lang3.ArrayUtils; - -public class MySqlSslCaCertificateSourceAcceptanceTest extends MySqlSourceAcceptanceTest { - - private static final String PASSWORD = "Passw0rd"; - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCaCertificate()) - .put("client_key_password", PASSWORD) - .build()) - .build(); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslFullCertificateSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslFullCertificateSourceAcceptanceTest.java deleted file mode 100644 index d9f325d9db31..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslFullCertificateSourceAcceptanceTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import org.apache.commons.lang3.ArrayUtils; - -public class MySqlSslFullCertificateSourceAcceptanceTest extends MySqlSourceAcceptanceTest { - - private static final String PASSWORD = "Passw0rd"; - - @Override - protected ContainerModifier[] getContainerModifiers() { - return ArrayUtils.toArray(ContainerModifier.ROOT_AND_SERVER_CERTIFICATES, ContainerModifier.CLIENT_CERTITICATE); - } - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCertificates().caCertificate()) - .put("client_certificate", testdb.getCertificates().clientCertificate()) - .put("client_key", testdb.getCertificates().clientKey()) - .put("client_key_password", PASSWORD) - .build()) - .build(); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslSourceAcceptanceTest.java deleted file mode 100644 index 5f46e43808e4..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/MySqlSslSourceAcceptanceTest.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; - -public class MySqlSslSourceAcceptanceTest extends MySqlSourceAcceptanceTest { - - @Override - protected JsonNode getConfig() { - return testdb.integrationTestConfigBuilder() - .withStandardReplication() - .withSsl(ImmutableMap.builder().put(JdbcUtils.MODE_KEY, "required").build()) - .build(); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshKeyMySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshKeyMySqlSourceAcceptanceTest.java deleted file mode 100644 index 7d5f060f34c2..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshKeyMySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import java.nio.file.Path; - -public class SshKeyMySqlSourceAcceptanceTest extends AbstractSshMySqlSourceAcceptanceTest { - - @Override - public Path getConfigFilePath() { - return Path.of("secrets/ssh-key-repl-config.json"); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshPasswordMySqlSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshPasswordMySqlSourceAcceptanceTest.java deleted file mode 100644 index 998e304d7145..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/java/io/airbyte/integrations/io/airbyte/integration_tests/sources/SshPasswordMySqlSourceAcceptanceTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.io.airbyte.integration_tests.sources; - -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import io.airbyte.cdk.integrations.base.Source; -import io.airbyte.cdk.integrations.base.ssh.SshBastionContainer; -import io.airbyte.cdk.integrations.base.ssh.SshTunnel; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.integrations.source.mysql.MySqlSource; -import java.nio.file.Path; -import org.junit.jupiter.api.Test; - -public class SshPasswordMySqlSourceAcceptanceTest extends AbstractSshMySqlSourceAcceptanceTest { - - @Override - public Path getConfigFilePath() { - return Path.of("secrets/ssh-pwd-repl-config.json"); - } - - @Test - public void sshTimeoutExceptionMarkAsConfigErrorTest() throws Exception { - try (final var testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8, ContainerModifier.NETWORK)) { - final SshBastionContainer bastion = new SshBastionContainer(); - bastion.initAndStartBastion(testdb.getContainer().getNetwork()); - final var config = testdb.integrationTestConfigBuilder() - .withoutSsl() - .with("tunnel_method", bastion.getTunnelMethod(SshTunnel.TunnelMethod.SSH_PASSWORD_AUTH, true)) - .build(); - bastion.stopAndClose(); - - final Source sshWrappedSource = MySqlSource.sshWrappedSource(new MySqlSource()); - final Exception exception = assertThrows(ConfigErrorException.class, () -> sshWrappedSource.discover(config)); - - final String expectedMessage = - "Timed out while opening a SSH Tunnel. Please double check the given SSH configurations and try again."; - final String actualMessage = exception.getMessage(); - assertTrue(actualMessage.contains(expectedMessage)); - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/dummy_config.json b/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/dummy_config.json deleted file mode 100644 index e17733f16b23..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/dummy_config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "host": "default", - "port": 5555, - "database": "default", - "username": "default", - "replication_method": { "method": "STANDARD" } -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_cloud_spec.json b/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_cloud_spec.json deleted file mode 100644 index b76358180e65..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_cloud_spec.json +++ /dev/null @@ -1,343 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/mysql", - "connectionSpecification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MySql Source Spec", - "type": "object", - "required": ["host", "port", "database", "username", "replication_method"], - "properties": { - "host": { - "description": "The host name of the database.", - "title": "Host", - "type": "string", - "order": 0 - }, - "port": { - "description": "The port to connect to.", - "title": "Port", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 3306, - "examples": ["3306"], - "order": 1 - }, - "database": { - "description": "The database name.", - "title": "Database", - "type": "string", - "order": 2 - }, - "username": { - "description": "The username which is used to access the database.", - "title": "Username", - "type": "string", - "order": 3 - }, - "password": { - "description": "The password associated with the username.", - "title": "Password", - "type": "string", - "airbyte_secret": true, - "order": 4, - "always_show": true - }, - "jdbc_url_params": { - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). For more information read about JDBC URL parameters.", - "title": "JDBC URL Parameters (Advanced)", - "type": "string", - "order": 5 - }, - "ssl_mode": { - "title": "SSL modes", - "description": "SSL connection modes. Read more in the docs.", - "type": "object", - "order": 7, - "oneOf": [ - { - "title": "preferred", - "description": "Automatically attempt SSL connection. If the MySQL server does not support SSL, continue with a regular connection.", - "required": ["mode"], - "properties": { - "mode": { "type": "string", "const": "preferred", "order": 0 } - } - }, - { - "title": "required", - "description": "Always connect with SSL. If the MySQL server doesn’t support SSL, the connection will not be established. Certificate Authority (CA) and Hostname are not verified.", - "required": ["mode"], - "properties": { - "mode": { "type": "string", "const": "required", "order": 0 } - } - }, - { - "title": "Verify CA", - "description": "Always connect with SSL. Verifies CA, but allows connection even if Hostname does not match.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { "type": "string", "const": "verify_ca", "order": 0 }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - }, - { - "title": "Verify Identity", - "description": "Always connect with SSL. Verify both CA and Hostname.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_identity", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - } - ], - "default": "required" - }, - "replication_method": { - "type": "object", - "title": "Update Method", - "description": "Configures how data is extracted from the database.", - "order": 8, - "default": "CDC", - "display_type": "radio", - "oneOf": [ - { - "title": "Read Changes using Binary Log (CDC)", - "description": "Recommended - Incrementally reads new inserts, updates, and deletes using the MySQL binary log. This must be enabled on your database.", - "required": ["method"], - "properties": { - "method": { "type": "string", "const": "CDC", "order": 0 }, - "initial_waiting_seconds": { - "type": "integer", - "title": "Initial Waiting Time in Seconds (Advanced)", - "description": "The amount of time the connector will wait when it launches to determine if there is new data to sync or not. Defaults to 300 seconds. Valid range: 120 seconds to 1200 seconds. Read about initial waiting time.", - "default": 300, - "min": 120, - "max": 1200, - "order": 1, - "always_show": true - }, - "server_time_zone": { - "type": "string", - "title": "Configured server timezone for the MySQL source (Advanced)", - "description": "Enter the configured MySQL server timezone. This should only be done if the configured timezone in your MySQL instance does not conform to IANNA standard.", - "order": 2, - "always_show": true - }, - "invalid_cdc_cursor_position_behavior": { - "type": "string", - "title": "Invalid CDC position behavior (Advanced)", - "description": "Determines whether Airbyte should fail or re-sync data in case of an stale/invalid cursor value into the WAL. If 'Fail sync' is chosen, a user will have to manually reset the connection before being able to continue syncing data. If 'Re-sync data' is chosen, Airbyte will automatically trigger a refresh but could lead to higher cloud costs and data loss.", - "enum": ["Fail sync", "Re-sync data"], - "default": "Fail sync", - "order": 3, - "always_show": true - }, - "initial_load_timeout_hours": { - "type": "integer", - "title": "Initial Load Timeout in Hours (Advanced)", - "description": "The amount of time an initial load is allowed to continue for before catching up on CDC logs.", - "default": 8, - "min": 4, - "max": 24, - "order": 4, - "always_show": true - } - } - }, - { - "title": "Scan Changes with User Defined Cursor", - "description": "Incrementally detects new inserts and updates using the cursor column chosen when configuring a connection (e.g. created_at, updated_at).", - "required": ["method"], - "properties": { - "method": { "type": "string", "const": "STANDARD", "order": 0 } - } - } - ] - }, - "tunnel_method": { - "type": "object", - "title": "SSH Tunnel Method", - "description": "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.", - "oneOf": [ - { - "title": "No Tunnel", - "required": ["tunnel_method"], - "properties": { - "tunnel_method": { - "description": "No ssh tunnel needed to connect to database", - "type": "string", - "const": "NO_TUNNEL", - "order": 0 - } - } - }, - { - "title": "SSH Key Authentication", - "required": [ - "tunnel_method", - "tunnel_host", - "tunnel_port", - "tunnel_user", - "ssh_key" - ], - "properties": { - "tunnel_method": { - "description": "Connect through a jump server tunnel host using username and ssh key", - "type": "string", - "const": "SSH_KEY_AUTH", - "order": 0 - }, - "tunnel_host": { - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel.", - "type": "string", - "order": 1 - }, - "tunnel_port": { - "title": "SSH Connection Port", - "description": "Port on the proxy/jump server that accepts inbound ssh connections.", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 22, - "examples": ["22"], - "order": 2 - }, - "tunnel_user": { - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host.", - "type": "string", - "order": 3 - }, - "ssh_key": { - "title": "SSH Private Key", - "description": "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )", - "type": "string", - "airbyte_secret": true, - "multiline": true, - "order": 4 - } - } - }, - { - "title": "Password Authentication", - "required": [ - "tunnel_method", - "tunnel_host", - "tunnel_port", - "tunnel_user", - "tunnel_user_password" - ], - "properties": { - "tunnel_method": { - "description": "Connect through a jump server tunnel host using username and password authentication", - "type": "string", - "const": "SSH_PASSWORD_AUTH", - "order": 0 - }, - "tunnel_host": { - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel.", - "type": "string", - "order": 1 - }, - "tunnel_port": { - "title": "SSH Connection Port", - "description": "Port on the proxy/jump server that accepts inbound ssh connections.", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 22, - "examples": ["22"], - "order": 2 - }, - "tunnel_user": { - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host", - "type": "string", - "order": 3 - }, - "tunnel_user_password": { - "title": "Password", - "description": "OS-level password for logging into the jump server host", - "type": "string", - "airbyte_secret": true, - "order": 4 - } - } - } - ] - } - } - }, - "supportsNormalization": false, - "supportsDBT": false, - "supported_destination_sync_modes": [] -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_oss_spec.json b/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_oss_spec.json deleted file mode 100644 index d45898990ba5..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/expected_oss_spec.json +++ /dev/null @@ -1,367 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/mysql", - "connectionSpecification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MySql Source Spec", - "type": "object", - "required": ["host", "port", "database", "username", "replication_method"], - "properties": { - "host": { - "description": "The host name of the database.", - "title": "Host", - "type": "string", - "order": 0 - }, - "port": { - "description": "The port to connect to.", - "title": "Port", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 3306, - "examples": ["3306"], - "order": 1 - }, - "database": { - "description": "The database name.", - "title": "Database", - "type": "string", - "order": 2 - }, - "username": { - "description": "The username which is used to access the database.", - "title": "Username", - "type": "string", - "order": 3 - }, - "password": { - "description": "The password associated with the username.", - "title": "Password", - "type": "string", - "airbyte_secret": true, - "order": 4, - "always_show": true - }, - "jdbc_url_params": { - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). For more information read about JDBC URL parameters.", - "title": "JDBC URL Parameters (Advanced)", - "type": "string", - "order": 5 - }, - "ssl": { - "title": "SSL Connection", - "description": "Encrypt data using SSL.", - "type": "boolean", - "default": true, - "order": 6 - }, - "ssl_mode": { - "title": "SSL modes", - "description": "SSL connection modes. Read more in the docs.", - "type": "object", - "order": 7, - "oneOf": [ - { - "title": "preferred", - "description": "Automatically attempt SSL connection. If the MySQL server does not support SSL, continue with a regular connection.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "preferred", - "order": 0 - } - } - }, - { - "title": "required", - "description": "Always connect with SSL. If the MySQL server doesn’t support SSL, the connection will not be established. Certificate Authority (CA) and Hostname are not verified.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "required", - "order": 0 - } - } - }, - { - "title": "Verify CA", - "description": "Always connect with SSL. Verifies CA, but allows connection even if Hostname does not match.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_ca", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - }, - { - "title": "Verify Identity", - "description": "Always connect with SSL. Verify both CA and Hostname.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_identity", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - } - ] - }, - "replication_method": { - "type": "object", - "title": "Update Method", - "description": "Configures how data is extracted from the database.", - "order": 8, - "default": "CDC", - "display_type": "radio", - "oneOf": [ - { - "title": "Read Changes using Binary Log (CDC)", - "description": "Recommended - Incrementally reads new inserts, updates, and deletes using the MySQL binary log. This must be enabled on your database.", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "CDC", - "order": 0 - }, - "initial_waiting_seconds": { - "type": "integer", - "title": "Initial Waiting Time in Seconds (Advanced)", - "description": "The amount of time the connector will wait when it launches to determine if there is new data to sync or not. Defaults to 300 seconds. Valid range: 120 seconds to 1200 seconds. Read about initial waiting time.", - "default": 300, - "min": 120, - "max": 1200, - "order": 1, - "always_show": true - }, - "server_time_zone": { - "type": "string", - "title": "Configured server timezone for the MySQL source (Advanced)", - "description": "Enter the configured MySQL server timezone. This should only be done if the configured timezone in your MySQL instance does not conform to IANNA standard.", - "order": 2, - "always_show": true - }, - "invalid_cdc_cursor_position_behavior": { - "type": "string", - "title": "Invalid CDC position behavior (Advanced)", - "description": "Determines whether Airbyte should fail or re-sync data in case of an stale/invalid cursor value into the WAL. If 'Fail sync' is chosen, a user will have to manually reset the connection before being able to continue syncing data. If 'Re-sync data' is chosen, Airbyte will automatically trigger a refresh but could lead to higher cloud costs and data loss.", - "enum": ["Fail sync", "Re-sync data"], - "default": "Fail sync", - "order": 3, - "always_show": true - }, - "initial_load_timeout_hours": { - "type": "integer", - "title": "Initial Load Timeout in Hours (Advanced)", - "description": "The amount of time an initial load is allowed to continue for before catching up on CDC logs.", - "default": 8, - "min": 4, - "max": 24, - "order": 4, - "always_show": true - } - } - }, - { - "title": "Scan Changes with User Defined Cursor", - "description": "Incrementally detects new inserts and updates using the cursor column chosen when configuring a connection (e.g. created_at, updated_at).", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "STANDARD", - "order": 0 - } - } - } - ] - }, - "tunnel_method": { - "type": "object", - "title": "SSH Tunnel Method", - "description": "Whether to initiate an SSH tunnel before connecting to the database, and if so, which kind of authentication to use.", - "oneOf": [ - { - "title": "No Tunnel", - "required": ["tunnel_method"], - "properties": { - "tunnel_method": { - "description": "No ssh tunnel needed to connect to database", - "type": "string", - "const": "NO_TUNNEL", - "order": 0 - } - } - }, - { - "title": "SSH Key Authentication", - "required": [ - "tunnel_method", - "tunnel_host", - "tunnel_port", - "tunnel_user", - "ssh_key" - ], - "properties": { - "tunnel_method": { - "description": "Connect through a jump server tunnel host using username and ssh key", - "type": "string", - "const": "SSH_KEY_AUTH", - "order": 0 - }, - "tunnel_host": { - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel.", - "type": "string", - "order": 1 - }, - "tunnel_port": { - "title": "SSH Connection Port", - "description": "Port on the proxy/jump server that accepts inbound ssh connections.", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 22, - "examples": ["22"], - "order": 2 - }, - "tunnel_user": { - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host.", - "type": "string", - "order": 3 - }, - "ssh_key": { - "title": "SSH Private Key", - "description": "OS-level user account ssh key credentials in RSA PEM format ( created with ssh-keygen -t rsa -m PEM -f myuser_rsa )", - "type": "string", - "airbyte_secret": true, - "multiline": true, - "order": 4 - } - } - }, - { - "title": "Password Authentication", - "required": [ - "tunnel_method", - "tunnel_host", - "tunnel_port", - "tunnel_user", - "tunnel_user_password" - ], - "properties": { - "tunnel_method": { - "description": "Connect through a jump server tunnel host using username and password authentication", - "type": "string", - "const": "SSH_PASSWORD_AUTH", - "order": 0 - }, - "tunnel_host": { - "title": "SSH Tunnel Jump Server Host", - "description": "Hostname of the jump server host that allows inbound ssh tunnel.", - "type": "string", - "order": 1 - }, - "tunnel_port": { - "title": "SSH Connection Port", - "description": "Port on the proxy/jump server that accepts inbound ssh connections.", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 22, - "examples": ["22"], - "order": 2 - }, - "tunnel_user": { - "title": "SSH Login Username", - "description": "OS-level username for logging into the jump server host", - "type": "string", - "order": 3 - }, - "tunnel_user_password": { - "title": "Password", - "description": "OS-level password for logging into the jump server host", - "type": "string", - "airbyte_secret": true, - "order": 4 - } - } - } - ] - } - } - }, - "supported_destination_sync_modes": [] -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/test.png b/airbyte-integrations/connectors/source-mysql/src/test-integration/resources/test.png deleted file mode 100644 index ca452bd25e3ceabaac85a0cbb061cd49e6ab727f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17018 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∾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 diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcConfigurationHelperTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcConfigurationHelperTest.java deleted file mode 100644 index 6c6a370c15fd..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcConfigurationHelperTest.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.cdc.CdcConfigurationHelper; -import java.util.Collections; -import java.util.Map; -import org.junit.jupiter.api.Test; - -public class CdcConfigurationHelperTest { - - @Test - void testServerTimeConfig() { - final JsonNode emptyConfig = Jsons.jsonNode(Collections.emptyMap()); - assertDoesNotThrow(() -> CdcConfigurationHelper.checkServerTimeZoneConfig(emptyConfig)); - - final JsonNode normalConfig = Jsons.jsonNode(Map.of("replication_method", - Map.of("method", "CDC", "server_time_zone", "America/Los_Angeles"))); - assertDoesNotThrow(() -> CdcConfigurationHelper.checkServerTimeZoneConfig(normalConfig)); - - final JsonNode invalidConfig = Jsons.jsonNode(Map.of("replication_method", - Map.of("method", "CDC", "server_time_zone", "CEST"))); - assertThrows(IllegalArgumentException.class, () -> CdcConfigurationHelper.checkServerTimeZoneConfig(invalidConfig)); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java deleted file mode 100644 index 85ab5a0f4c7a..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceTest.java +++ /dev/null @@ -1,930 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static io.airbyte.cdk.integrations.debezium.DebeziumIteratorConstants.SYNC_CHECKPOINT_RECORDS_PROPERTY; -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_DELETED_AT; -import static io.airbyte.cdk.integrations.debezium.internals.DebeziumEventConverter.CDC_UPDATED_AT; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_DEFAULT_CURSOR; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_LOG_FILE; -import static io.airbyte.integrations.source.mysql.MySqlSource.CDC_LOG_POS; -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.FAIL_SYNC_OPTION; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.IS_COMPRESSED; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.MYSQL_CDC_OFFSET; -import static io.airbyte.integrations.source.mysql.cdc.MysqlCdcStateConstants.MYSQL_DB_HISTORY; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStateManager.PRIMARY_KEY_STATE_TYPE; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStateManager.STATE_TYPE_KEY; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -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.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Iterables; -import com.google.common.collect.Lists; -import com.google.common.collect.Streams; -import io.airbyte.cdk.db.jdbc.DefaultJdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.integrations.debezium.CdcSourceTest; -import io.airbyte.cdk.integrations.debezium.internals.AirbyteSchemaHistoryStorage; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.util.AutoCloseableIterator; -import io.airbyte.commons.util.AutoCloseableIterators; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcProperties; -import io.airbyte.integrations.source.mysql.cdc.MySqlCdcTargetPosition; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus.Status; -import io.airbyte.protocol.models.v0.AirbyteGlobalState; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.AirbyteRecordMessage; -import io.airbyte.protocol.models.v0.AirbyteRecordMessageMeta; -import io.airbyte.protocol.models.v0.AirbyteRecordMessageMetaChange; -import io.airbyte.protocol.models.v0.AirbyteRecordMessageMetaChange.Change; -import io.airbyte.protocol.models.v0.AirbyteRecordMessageMetaChange.Reason; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import io.airbyte.protocol.models.v0.AirbyteStream; -import io.airbyte.protocol.models.v0.AirbyteStreamState; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.StreamDescriptor; -import io.airbyte.protocol.models.v0.SyncMode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Properties; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; -import java.util.stream.Collectors; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -@Order(1) -@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NULL_ON_SOME_PATH") -public class CdcMysqlSourceTest extends CdcSourceTest { - - private static final String INVALID_TIMEZONE_CEST = "CEST"; - - private static final Random RANDOM = new Random(); - - private static final String TEST_DATE_STREAM_NAME = "TEST_DATE_TABLE"; - private static final String COL_DATE_TIME = "CAR_DATE"; - private static final List DATE_TIME_RECORDS = ImmutableList.of( - Jsons.jsonNode(ImmutableMap.of(COL_ID, 120, COL_DATE_TIME, "'2023-00-00 20:37:47'"))); - - @Override - protected void assertExpectedStateMessageCountMatches(final List stateMessages, long totalCount) { - AtomicLong count = new AtomicLong(0L); - stateMessages.stream().forEach( - stateMessage -> count.addAndGet(stateMessage.getSourceStats() != null ? stateMessage.getSourceStats().getRecordCount().longValue() : 0L)); - assertEquals(totalCount, count.get()); - } - - @Override - protected MySQLTestDatabase createTestDatabase() { - return MySQLTestDatabase.in(BaseImage.MYSQL_8, ContainerModifier.INVALID_TIMEZONE_CEST).withCdcPermissions(); - } - - @Override - protected MySqlSource source() { - return new MySqlSource(); - } - - @Override - protected JsonNode config() { - return testdb.testConfigBuilder() - .withCdcReplication() - .with(SYNC_CHECKPOINT_RECORDS_PROPERTY, 1) - .build(); - } - - protected void purgeAllBinaryLogs() { - testdb.with("RESET MASTER;"); - } - - @Override - protected String createSchemaSqlFmt() { - return "CREATE DATABASE IF NOT EXISTS `%s`;"; - } - - @Override - protected String createTableSqlFmt() { - return "CREATE TABLE `%s`.`%s`(%s);"; - } - - @Override - protected String modelsSchema() { - return getDatabaseName(); - } - - @Override - protected String randomSchema() { - return getDatabaseName(); - } - - protected String getDatabaseName() { - return testdb.getDatabaseName(); - } - - @Override - protected MySqlCdcTargetPosition cdcLatestTargetPosition() { - return MySqlCdcTargetPosition.targetPosition(new DefaultJdbcDatabase(testdb.getDataSource())); - } - - @Override - protected MySqlCdcTargetPosition extractPosition(final JsonNode record) { - return new MySqlCdcTargetPosition(record.get(CDC_LOG_FILE).asText(), record.get(CDC_LOG_POS).asLong()); - } - - @Override - protected void assertNullCdcMetaData(final JsonNode data) { - assertNull(data.get(CDC_LOG_FILE)); - assertNull(data.get(CDC_LOG_POS)); - assertNull(data.get(CDC_UPDATED_AT)); - assertNull(data.get(CDC_DELETED_AT)); - assertNull(data.get(CDC_DEFAULT_CURSOR)); - } - - @Override - protected void assertCdcMetaData(final JsonNode data, final boolean deletedAtNull) { - assertNotNull(data.get(CDC_LOG_FILE)); - assertNotNull(data.get(CDC_LOG_POS)); - assertNotNull(data.get(CDC_UPDATED_AT)); - assertNotNull(data.get(CDC_DEFAULT_CURSOR)); - if (deletedAtNull) { - assertTrue(data.get(CDC_DELETED_AT).isNull()); - } else { - assertFalse(data.get(CDC_DELETED_AT).isNull()); - } - } - - @Override - protected void removeCDCColumns(final ObjectNode data) { - data.remove(CDC_LOG_FILE); - data.remove(CDC_LOG_POS); - data.remove(CDC_UPDATED_AT); - data.remove(CDC_DELETED_AT); - data.remove(CDC_DEFAULT_CURSOR); - } - - @Override - protected void addCdcMetadataColumns(final AirbyteStream stream) { - final ObjectNode jsonSchema = (ObjectNode) stream.getJsonSchema(); - final ObjectNode properties = (ObjectNode) jsonSchema.get("properties"); - - final JsonNode airbyteIntegerType = Jsons.jsonNode(ImmutableMap.of("type", "number", "airbyte_type", "integer")); - final JsonNode numberType = Jsons.jsonNode(ImmutableMap.of("type", "number")); - final JsonNode stringType = Jsons.jsonNode(ImmutableMap.of("type", "string")); - properties.set(CDC_LOG_FILE, stringType); - properties.set(CDC_LOG_POS, numberType); - properties.set(CDC_UPDATED_AT, stringType); - properties.set(CDC_DELETED_AT, stringType); - properties.set(CDC_DEFAULT_CURSOR, airbyteIntegerType); - } - - @Override - protected void addCdcDefaultCursorField(final AirbyteStream stream) { - if (stream.getSupportedSyncModes().contains(SyncMode.INCREMENTAL)) { - stream.setDefaultCursorField(ImmutableList.of(CDC_DEFAULT_CURSOR)); - } - } - - @Override - protected void writeRecords( - final JsonNode recordJson, - final String dbName, - final String streamName, - final String idCol, - final String makeIdCol, - final String modelCol) { - testdb.with("INSERT INTO `%s` .`%s` (%s, %s, %s) VALUES (%s, %s, '%s');", dbName, streamName, - idCol, makeIdCol, modelCol, - recordJson.get(idCol).asInt(), recordJson.get(makeIdCol).asInt(), - recordJson.get(modelCol).asText()); - } - - @Override - protected void deleteMessageOnIdCol(final String streamName, final String idCol, final int idValue) { - testdb.with("DELETE FROM `%s`.`%s` WHERE %s = %s", modelsSchema(), streamName, idCol, idValue); - } - - @Override - protected void deleteCommand(final String streamName) { - testdb.with("DELETE FROM `%s`.`%s`", modelsSchema(), streamName); - } - - @Override - protected void updateCommand(final String streamName, final String modelCol, final String modelVal, final String idCol, final int idValue) { - testdb.with("UPDATE `%s`.`%s` SET %s = '%s' WHERE %s = %s", modelsSchema(), streamName, - modelCol, modelVal, COL_ID, 11); - } - - @Override - protected boolean supportResumableFullRefresh() { - return true; - } - - @Override - protected void addIsResumableFlagForNonPkTable(final AirbyteStream stream) { - stream.setIsResumable(false); - } - - @Test - protected void syncWithReplicationClientPrivilegeRevokedFailsCheck() throws Exception { - testdb.with("REVOKE REPLICATION CLIENT ON *.* FROM %s@'%%';", testdb.getUserName()); - final AirbyteConnectionStatus status = source().check(config()); - final String expectedErrorMessage = "Please grant REPLICATION CLIENT privilege, so that binary log files are available" - + " for CDC mode."; - assertTrue(status.getStatus().equals(Status.FAILED)); - assertTrue(status.getMessage().contains(expectedErrorMessage)); - } - - @Test - protected void syncShouldHandlePurgedLogsGracefully() throws Exception { - - final int recordsToCreate = 20; - // first batch of records. 20 created here and 6 created in setup method. - for (int recordsCreated = 0; recordsCreated < recordsToCreate; recordsCreated++) { - final JsonNode record = - Jsons.jsonNode(ImmutableMap - .of(COL_ID, 100 + recordsCreated, COL_MAKE_ID, 1, COL_MODEL, - "F-" + recordsCreated)); - writeModelRecord(record); - } - - final AutoCloseableIterator firstBatchIterator = source() - .read(config(), getConfiguredCatalog(), null); - final List dataFromFirstBatch = AutoCloseableIterators - .toListAndClose(firstBatchIterator); - final List stateAfterFirstBatch = extractStateMessages(dataFromFirstBatch); - assertStateForSyncShouldHandlePurgedLogsGracefully(stateAfterFirstBatch, 1); - final Set recordsFromFirstBatch = extractRecordMessages( - dataFromFirstBatch); - - final int recordsCreatedBeforeTestCount = MODEL_RECORDS.size(); - assertEquals((recordsCreatedBeforeTestCount + recordsToCreate), recordsFromFirstBatch.size()); - // sometimes there can be more than one of these at the end of the snapshot and just before the - // first incremental. - final Set recordsFromFirstBatchWithoutDuplicates = removeDuplicates( - recordsFromFirstBatch); - - assertTrue(recordsCreatedBeforeTestCount < recordsFromFirstBatchWithoutDuplicates.size(), - "Expected first sync to include records created while the test was running."); - - // second batch of records again 20 being created - for (int recordsCreated = 0; recordsCreated < recordsToCreate; recordsCreated++) { - final JsonNode record = - Jsons.jsonNode(ImmutableMap - .of(COL_ID, 200 + recordsCreated, COL_MAKE_ID, 1, COL_MODEL, - "F-" + recordsCreated)); - writeModelRecord(record); - } - - purgeAllBinaryLogs(); - - final JsonNode state = Jsons.jsonNode(Collections.singletonList(stateAfterFirstBatch.get(stateAfterFirstBatch.size() - 1))); - final AutoCloseableIterator secondBatchIterator = source() - .read(config(), getConfiguredCatalog(), state); - final List dataFromSecondBatch = AutoCloseableIterators - .toListAndClose(secondBatchIterator); - - final List stateAfterSecondBatch = extractStateMessages(dataFromSecondBatch); - assertStateForSyncShouldHandlePurgedLogsGracefully(stateAfterSecondBatch, 2); - - final Set recordsFromSecondBatch = extractRecordMessages( - dataFromSecondBatch); - assertEquals((recordsToCreate * 2) + recordsCreatedBeforeTestCount, recordsFromSecondBatch.size(), - "Expected 46 records to be replicated in the second sync."); - - JsonNode failSyncConfig = testdb.testConfigBuilder() - .withCdcReplication(FAIL_SYNC_OPTION) - .with(SYNC_CHECKPOINT_RECORDS_PROPERTY, 1) - .build(); - assertThrows(ConfigErrorException.class, () -> source().read(failSyncConfig, getConfiguredCatalog(), state)); - } - - /** - * This test verifies that multiple states are sent during the CDC process based on number of - * records. We can ensure that more than one `STATE` type of message is sent, but we are not able to - * assert the exact number of messages sent as depends on Debezium. - * - * @throws Exception Exception happening in the test. - */ - @Test - @Timeout(value = 5, - unit = TimeUnit.MINUTES) - protected void verifyCheckpointStatesByRecords() throws Exception { - // We require a huge amount of records, otherwise Debezium will notify directly the last offset. - final int recordsToCreate = 20_000; - - final AutoCloseableIterator firstBatchIterator = source() - .read(config(), getConfiguredCatalog(), null); - final List dataFromFirstBatch = AutoCloseableIterators - .toListAndClose(firstBatchIterator); - final List stateMessages = extractStateMessages(dataFromFirstBatch); - - // As first `read` operation is from snapshot, it would generate only one state message at the end - // of the process. - assertExpectedStateMessages(stateMessages); - - for (int recordsCreated = 0; recordsCreated < recordsToCreate; recordsCreated++) { - final JsonNode record = Jsons.jsonNode(ImmutableMap - .of(COL_ID, 200 + recordsCreated, COL_MAKE_ID, 1, COL_MODEL, "F-" + recordsCreated)); - writeModelRecord(record); - } - - final JsonNode stateAfterFirstSync = Jsons.jsonNode(Collections.singletonList(stateMessages.get(stateMessages.size() - 1))); - final AutoCloseableIterator secondBatchIterator = source() - .read(config(), getConfiguredCatalog(), stateAfterFirstSync); - final List dataFromSecondBatch = AutoCloseableIterators - .toListAndClose(secondBatchIterator); - assertEquals(recordsToCreate, extractRecordMessages(dataFromSecondBatch).size()); - final List stateMessagesCDC = extractStateMessages(dataFromSecondBatch); - assertTrue(stateMessagesCDC.size() > 1, "Generated only the final state."); - assertEquals(stateMessagesCDC.size(), stateMessagesCDC.stream().distinct().count(), "There are duplicated states."); - } - - @Override - protected void assertExpectedStateMessages(final List stateMessages) { - assertEquals(7, stateMessages.size()); - assertStateTypes(stateMessages, 4, supportResumableFullRefresh()); - } - - @Override - protected void assertExpectedStateMessagesForFullRefresh(final List stateMessages) { - // Full refresh will only send 6 state messages - one for each record (including the final one). - assertEquals(6, stateMessages.size()); - } - - protected void assertExpectedStateMessagesWithTotalCount(final List stateMessages, final long totalRecordCount) { - long actualRecordCount = 0L; - for (final AirbyteStateMessage message : stateMessages) { - actualRecordCount += message.getSourceStats().getRecordCount(); - } - assertEquals(actualRecordCount, totalRecordCount); - } - - @Override - protected void assertExpectedStateMessagesFromIncrementalSync(final List stateMessages) { - assertEquals(1, stateMessages.size()); - assertNotNull(stateMessages.get(0).getData()); - for (final AirbyteStateMessage stateMessage : stateMessages) { - assertNotNull(stateMessage.getData().get("cdc_state").get("state").get(MYSQL_CDC_OFFSET)); - assertNotNull(stateMessage.getData().get("cdc_state").get("state").get(MYSQL_DB_HISTORY)); - } - } - - private void assertStateForSyncShouldHandlePurgedLogsGracefully(final List stateMessages, final int syncNumber) { - if (syncNumber == 1) { - assertExpectedStateMessagesForRecordsProducedDuringAndAfterSync(stateMessages); - } else if (syncNumber == 2) { - // Sync number 2 uses the state from sync number 1 but before we trigger the sync 2 we purge the - // binary logs and as a result the validation of - // logs present on the server fails, and we trigger a sync from scratch - assertEquals(47, stateMessages.size()); - assertStateTypes(stateMessages, 44); - } else { - throw new RuntimeException("Unknown sync number"); - } - - } - - @Override - protected void assertExpectedStateMessagesForRecordsProducedDuringAndAfterSync(final List stateAfterFirstBatch) { - assertEquals(27, stateAfterFirstBatch.size()); - assertStateTypes(stateAfterFirstBatch, 24); - } - - @Override - protected void assertExpectedStateMessagesForNoData(final List stateMessages) { - assertEquals(2, stateMessages.size()); - } - - @Override - protected void validateStreamStateInResumableFullRefresh(final JsonNode streamStateToBeTested) { - // Pk should be pointing to the last element from MODEL_RECORDS table. - assertEquals("16", streamStateToBeTested.get("pk_val").asText()); - assertEquals("id", streamStateToBeTested.get("pk_name").asText()); - assertEquals("primary_key", streamStateToBeTested.get("state_type").asText()); - } - - private void assertStateTypes(final List stateMessages, final int indexTillWhichExpectPkState) { - assertStateTypes(stateMessages, indexTillWhichExpectPkState, false); - } - - private void assertStateTypes(final List stateMessages, - final int indexTillWhichExpectPkState, - boolean expectSharedStateChange) { - JsonNode sharedState = null; - - for (int i = 0; i < stateMessages.size(); i++) { - final AirbyteStateMessage stateMessage = stateMessages.get(i); - assertEquals(AirbyteStateType.GLOBAL, stateMessage.getType()); - final AirbyteGlobalState global = stateMessage.getGlobal(); - assertNotNull(global.getSharedState()); - if (Objects.isNull(sharedState)) { - sharedState = global.getSharedState(); - } else if (expectSharedStateChange && i == indexTillWhichExpectPkState) { - sharedState = global.getSharedState(); - } else if (i != stateMessages.size() - 1) { - assertEquals(sharedState, global.getSharedState()); - } - assertEquals(1, global.getStreamStates().size()); - final AirbyteStreamState streamState = global.getStreamStates().get(0); - if (i <= indexTillWhichExpectPkState) { - assertTrue(streamState.getStreamState().has(STATE_TYPE_KEY)); - assertEquals(PRIMARY_KEY_STATE_TYPE, streamState.getStreamState().get(STATE_TYPE_KEY).asText()); - } else { - assertFalse(streamState.getStreamState().has(STATE_TYPE_KEY)); - } - } - } - - @Override - protected void assertStateMessagesForNewTableSnapshotTest(final List stateMessages, - final AirbyteStateMessage stateMessageEmittedAfterFirstSyncCompletion) { - - // First message emitted in the WASS case is a CDC state message. This should have a different - // global state (LSN) as compared to the previous - // finishing state. The streams in snapshot phase should be the one that is completed at that point. - assertEquals(7, stateMessages.size()); - final AirbyteStateMessage cdcStateMessage = stateMessages.get(0); - assertNotEquals(stateMessageEmittedAfterFirstSyncCompletion.getGlobal().getSharedState(), cdcStateMessage.getGlobal().getSharedState()); - Set streamsInSnapshotState = cdcStateMessage.getGlobal().getStreamStates() - .stream() - .map(AirbyteStreamState::getStreamDescriptor) - .collect(Collectors.toSet()); - assertEquals(1, streamsInSnapshotState.size()); - assertTrue(streamsInSnapshotState.contains(new StreamDescriptor().withName(MODELS_STREAM_NAME).withNamespace(getDatabaseName()))); - - for (int i = 1; i <= 5; i++) { - final AirbyteStateMessage stateMessage = stateMessages.get(i); - assertEquals(AirbyteStateType.GLOBAL, stateMessage.getType()); - // Shared state should not be the same as the first (CDC) state message as it should not change in - // initial sync. - assertEquals(cdcStateMessage.getGlobal().getSharedState(), stateMessage.getGlobal().getSharedState()); - streamsInSnapshotState.clear(); - streamsInSnapshotState = stateMessage.getGlobal().getStreamStates() - .stream() - .map(AirbyteStreamState::getStreamDescriptor) - .collect(Collectors.toSet()); - assertEquals(2, streamsInSnapshotState.size()); - assertTrue( - streamsInSnapshotState.contains(new StreamDescriptor().withName(MODELS_STREAM_NAME + "_random").withNamespace(randomSchema()))); - assertTrue(streamsInSnapshotState.contains(new StreamDescriptor().withName(MODELS_STREAM_NAME).withNamespace(getDatabaseName()))); - - stateMessage.getGlobal().getStreamStates().forEach(s -> { - final JsonNode streamState = s.getStreamState(); - if (s.getStreamDescriptor().equals(new StreamDescriptor().withName(MODELS_STREAM_NAME + "_random").withNamespace(randomSchema()))) { - assertEquals(PRIMARY_KEY_STATE_TYPE, streamState.get(STATE_TYPE_KEY).asText()); - } else if (s.getStreamDescriptor().equals(new StreamDescriptor().withName(MODELS_STREAM_NAME).withNamespace(getDatabaseName()))) { - assertFalse(streamState.has(STATE_TYPE_KEY)); - } else { - throw new RuntimeException("Unknown stream"); - } - }); - } - - // The last message emitted should indicate that initial PK load has finished for both streams. - final AirbyteStateMessage stateMessageEmittedAfterSecondSyncCompletion = stateMessages.get(6); - assertEquals(AirbyteStateType.GLOBAL, stateMessageEmittedAfterSecondSyncCompletion.getType()); - assertEquals(cdcStateMessage.getGlobal().getSharedState(), - stateMessageEmittedAfterSecondSyncCompletion.getGlobal().getSharedState()); - streamsInSnapshotState.clear(); - streamsInSnapshotState = stateMessageEmittedAfterSecondSyncCompletion.getGlobal().getStreamStates() - .stream() - .map(AirbyteStreamState::getStreamDescriptor) - .collect(Collectors.toSet()); - assertEquals(2, streamsInSnapshotState.size()); - assertTrue( - streamsInSnapshotState.contains(new StreamDescriptor().withName(MODELS_STREAM_NAME + "_random").withNamespace(randomSchema()))); - assertTrue(streamsInSnapshotState.contains(new StreamDescriptor().withName(MODELS_STREAM_NAME).withNamespace(getDatabaseName()))); - stateMessageEmittedAfterSecondSyncCompletion.getGlobal().getStreamStates().forEach(s -> { - final JsonNode streamState = s.getStreamState(); - assertFalse(streamState.has(STATE_TYPE_KEY)); - }); - } - - @Test - @Timeout(value = 60) - public void syncWouldWorkWithDBWithInvalidTimezone() throws Exception { - final String systemTimeZone = "@@system_time_zone"; - final JdbcDatabase jdbcDatabase = source().createDatabase(config()); - final Properties properties = MySqlCdcProperties.getDebeziumProperties(jdbcDatabase); - final String databaseTimezone = jdbcDatabase.unsafeQuery(String.format("SELECT %s;", systemTimeZone)).toList().get(0).get(systemTimeZone) - .asText(); - final String debeziumEngineTimezone = properties.getProperty("database.connectionTimeZone"); - - assertEquals(INVALID_TIMEZONE_CEST, databaseTimezone); - assertEquals("America/Los_Angeles", debeziumEngineTimezone); - - final AutoCloseableIterator read = source() - .read(config(), getConfiguredCatalog(), null); - - final List actualRecords = AutoCloseableIterators.toListAndClose(read); - - final Set recordMessages = extractRecordMessages(actualRecords); - final List stateMessages = extractStateMessages(actualRecords); - - assertExpectedRecords(new HashSet<>(MODEL_RECORDS), recordMessages); - assertExpectedStateMessages(stateMessages); - assertExpectedStateMessagesWithTotalCount(stateMessages, 6); - } - - @Test - public void testCompositeIndexInitialLoad() throws Exception { - // Simulate adding a composite index by modifying the catalog. - final ConfiguredAirbyteCatalog configuredCatalog = Jsons.clone(getConfiguredCatalog()); - final List> primaryKeys = configuredCatalog.getStreams().get(0).getStream().getSourceDefinedPrimaryKey(); - primaryKeys.add(List.of("make_id")); - - final AutoCloseableIterator read1 = source() - .read(config(), configuredCatalog, null); - - final List actualRecords1 = AutoCloseableIterators.toListAndClose(read1); - - final Set recordMessages1 = extractRecordMessages(actualRecords1); - final List stateMessages1 = extractStateMessages(actualRecords1); - assertExpectedRecords(new HashSet<>(MODEL_RECORDS), recordMessages1); - assertExpectedStateMessages(stateMessages1); - assertExpectedStateMessagesWithTotalCount(stateMessages1, 6); - - // Re-run the sync with state associated with record w/ id = 15 (second to last record). - // We expect to read 2 records, since in the case of a composite PK we issue a >= query. - // We also expect 3 state records. One associated with the pk state, one to signify end of initial - // load, and - // the last one indicating the cdc position we have synced until. - final JsonNode state = Jsons.jsonNode(Collections.singletonList(stateMessages1.get(4))); - final AutoCloseableIterator read2 = source() - .read(config(), configuredCatalog, state); - - final List actualRecords2 = AutoCloseableIterators.toListAndClose(read2); - final Set recordMessages2 = extractRecordMessages(actualRecords2); - final List stateMessages2 = extractStateMessages(actualRecords2); - - assertExpectedRecords(new HashSet<>(MODEL_RECORDS.subList(4, 6)), recordMessages2); - assertEquals(3, stateMessages2.size()); - // In the second sync (WASS case), the first state message is emitted via debezium use case, which - // should still have the pk state encoded within. The second state message emitted will contain - // state from the initial - // sync and the last (3rd) state message will not have any pk state as the initial sync can now be - // considered complete. - assertStateTypes(stateMessages2, 1); - } - - // Remove all timestamp related fields in shared state. We want to make sure other information will - // not change. - private void pruneSharedStateTimestamp(final JsonNode rootNode) throws Exception { - ObjectMapper mapper = new ObjectMapper(); - - // Navigate to the specific node - JsonNode historyNode = rootNode.path("state").path("mysql_db_history"); - if (historyNode.isMissingNode()) { - return; // Node not found, nothing to do - } - String historyJson = historyNode.asText(); - JsonNode historyJsonNode = mapper.readTree(historyJson); - - ObjectNode objectNode = (ObjectNode) historyJsonNode; - objectNode.remove("ts_ms"); - - if (objectNode.has("position") && objectNode.get("position").has("ts_sec")) { - ((ObjectNode) objectNode.get("position")).remove("ts_sec"); - } - - JsonNode offsetNode = rootNode.path("state").path("mysql_cdc_offset"); - JsonNode offsetJsonNode = mapper.readTree(offsetNode.asText()); - if (offsetJsonNode.has("ts_sec")) { - ((ObjectNode) offsetJsonNode).remove("ts_sec"); - } - - // Replace the original string with the modified one - ((ObjectNode) rootNode.path("state")).put("mysql_db_history", mapper.writeValueAsString(historyJsonNode)); - ((ObjectNode) rootNode.path("state")).put("mysql_cdc_offset", mapper.writeValueAsString(offsetJsonNode)); - } - - @Test - public void testTwoStreamSync() throws Exception { - // Add another stream models_2 and read that one as well. - final ConfiguredAirbyteCatalog configuredCatalog = Jsons.clone(getConfiguredCatalog()); - - final List MODEL_RECORDS_2 = ImmutableList.of( - Jsons.jsonNode(ImmutableMap.of(COL_ID, 110, COL_MAKE_ID, 1, COL_MODEL, "Fiesta-2")), - Jsons.jsonNode(ImmutableMap.of(COL_ID, 120, COL_MAKE_ID, 1, COL_MODEL, "Focus-2")), - Jsons.jsonNode(ImmutableMap.of(COL_ID, 130, COL_MAKE_ID, 1, COL_MODEL, "Ranger-2")), - Jsons.jsonNode(ImmutableMap.of(COL_ID, 140, COL_MAKE_ID, 2, COL_MODEL, "GLA-2")), - Jsons.jsonNode(ImmutableMap.of(COL_ID, 150, COL_MAKE_ID, 2, COL_MODEL, "A 220-2")), - Jsons.jsonNode(ImmutableMap.of(COL_ID, 160, COL_MAKE_ID, 2, COL_MODEL, "E 350-2"))); - - testdb.with(createTableSqlFmt(), getDatabaseName(), MODELS_STREAM_NAME + "_2", - columnClause(ImmutableMap.of(COL_ID, "INTEGER", COL_MAKE_ID, "INTEGER", COL_MODEL, "VARCHAR(200)"), Optional.of(COL_ID))); - - for (final JsonNode recordJson : MODEL_RECORDS_2) { - writeRecords(recordJson, getDatabaseName(), MODELS_STREAM_NAME + "_2", COL_ID, - COL_MAKE_ID, COL_MODEL); - } - - final ConfiguredAirbyteStream airbyteStream = new ConfiguredAirbyteStream() - .withStream(CatalogHelpers.createAirbyteStream( - MODELS_STREAM_NAME + "_2", - getDatabaseName(), - Field.of(COL_ID, JsonSchemaType.INTEGER), - Field.of(COL_MAKE_ID, JsonSchemaType.INTEGER), - Field.of(COL_MODEL, JsonSchemaType.STRING)) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of(COL_ID)))); - airbyteStream.setSyncMode(SyncMode.INCREMENTAL); - - final List streams = configuredCatalog.getStreams(); - streams.add(airbyteStream); - configuredCatalog.withStreams(streams); - - final AutoCloseableIterator read1 = source() - .read(config(), configuredCatalog, null); - final List actualRecords1 = AutoCloseableIterators.toListAndClose(read1); - - final Set recordMessages1 = extractRecordMessages(actualRecords1); - final List stateMessages1 = extractStateMessages(actualRecords1); - assertEquals(13, stateMessages1.size()); - assertExpectedStateMessagesWithTotalCount(stateMessages1, 12); - - JsonNode sharedState = null; - StreamDescriptor firstStreamInState = null; - for (int i = 0; i < stateMessages1.size(); i++) { - final AirbyteStateMessage stateMessage = stateMessages1.get(i); - assertEquals(AirbyteStateType.GLOBAL, stateMessage.getType()); - final AirbyteGlobalState global = stateMessage.getGlobal(); - assertNotNull(global.getSharedState()); - if (Objects.isNull(sharedState)) { - ObjectMapper mapper = new ObjectMapper(); - sharedState = mapper.valueToTree(global.getSharedState()); - pruneSharedStateTimestamp(sharedState); - } else { - ObjectMapper mapper = new ObjectMapper(); - var newSharedState = mapper.valueToTree(global.getSharedState()); - pruneSharedStateTimestamp(newSharedState); - assertEquals(sharedState, newSharedState); - } - - if (Objects.isNull(firstStreamInState)) { - assertEquals(1, global.getStreamStates().size()); - firstStreamInState = global.getStreamStates().get(0).getStreamDescriptor(); - } - - if (i <= 4) { - // First 4 state messages are pk state - assertEquals(1, global.getStreamStates().size()); - final AirbyteStreamState streamState = global.getStreamStates().get(0); - assertTrue(streamState.getStreamState().has(STATE_TYPE_KEY)); - assertEquals(PRIMARY_KEY_STATE_TYPE, streamState.getStreamState().get(STATE_TYPE_KEY).asText()); - } else if (i == 5) { - // 5th state message is the final state message emitted for the stream - assertEquals(1, global.getStreamStates().size()); - final AirbyteStreamState streamState = global.getStreamStates().get(0); - assertFalse(streamState.getStreamState().has(STATE_TYPE_KEY)); - } else if (i <= 10) { - // 6th to 10th is the primary_key state message for the 2nd stream but final state message for 1st - // stream - assertEquals(2, global.getStreamStates().size()); - final StreamDescriptor finalFirstStreamInState = firstStreamInState; - global.getStreamStates().forEach(c -> { - if (c.getStreamDescriptor().equals(finalFirstStreamInState)) { - assertFalse(c.getStreamState().has(STATE_TYPE_KEY)); - } else { - assertTrue(c.getStreamState().has(STATE_TYPE_KEY)); - assertEquals(PRIMARY_KEY_STATE_TYPE, c.getStreamState().get(STATE_TYPE_KEY).asText()); - } - }); - } else { - // last 2 state messages don't contain primary_key info cause primary_key sync should be complete - assertEquals(2, global.getStreamStates().size()); - global.getStreamStates().forEach(c -> assertFalse(c.getStreamState().has(STATE_TYPE_KEY))); - } - } - - final Set names = new HashSet<>(STREAM_NAMES); - names.add(MODELS_STREAM_NAME + "_2"); - assertExpectedRecords(Streams.concat(MODEL_RECORDS_2.stream(), MODEL_RECORDS.stream()) - .collect(Collectors.toSet()), - recordMessages1, - names, - names, - getDatabaseName()); - - assertEquals(new StreamDescriptor().withName(MODELS_STREAM_NAME).withNamespace(getDatabaseName()), firstStreamInState); - - // Triggering a sync with a primary_key state for 1 stream and complete state for other stream - final AutoCloseableIterator read2 = source() - .read(config(), configuredCatalog, Jsons.jsonNode(Collections.singletonList(stateMessages1.get(6)))); - final List actualRecords2 = AutoCloseableIterators.toListAndClose(read2); - - final List stateMessages2 = extractStateMessages(actualRecords2); - - assertEquals(6, stateMessages2.size()); - // State was reset to the 7th; thus 5 remaining records were expected to be reloaded. - assertExpectedStateMessagesWithTotalCount(stateMessages2, 5); - for (int i = 0; i < stateMessages2.size(); i++) { - final AirbyteStateMessage stateMessage = stateMessages2.get(i); - assertEquals(AirbyteStateType.GLOBAL, stateMessage.getType()); - final AirbyteGlobalState global = stateMessage.getGlobal(); - assertNotNull(global.getSharedState()); - assertEquals(2, global.getStreamStates().size()); - - if (i <= 4) { - final StreamDescriptor finalFirstStreamInState = firstStreamInState; - global.getStreamStates().forEach(c -> { - // First 5 state messages are primary_key state for the stream that didn't complete primary_key sync - // the first time - if (c.getStreamDescriptor().equals(finalFirstStreamInState)) { - assertFalse(c.getStreamState().has(STATE_TYPE_KEY)); - } else { - assertTrue(c.getStreamState().has(STATE_TYPE_KEY)); - assertEquals(PRIMARY_KEY_STATE_TYPE, c.getStreamState().get(STATE_TYPE_KEY).asText()); - } - }); - } else { - // last state messages doesn't contain primary_key info cause primary_key sync should be complete - global.getStreamStates().forEach(c -> assertFalse(c.getStreamState().has(STATE_TYPE_KEY))); - } - } - - final Set recordMessages2 = extractRecordMessages(actualRecords2); - assertEquals(5, recordMessages2.size()); - assertExpectedRecords(new HashSet<>(MODEL_RECORDS_2.subList(1, MODEL_RECORDS_2.size())), - recordMessages2, - names, - names, - getDatabaseName()); - } - - /** - * This test creates lots of tables increasing the schema history size above the limit of forcing - * the {@link AirbyteSchemaHistoryStorage#read()} method to compress the schema history blob as part - * of the state message which allows us to test that the next sync is able to work fine when - * provided with a compressed blob in the state. - */ - @Test - @Timeout(value = 120) - public void testCompressedSchemaHistory() throws Exception { - createTablesToIncreaseSchemaHistorySize(); - final AutoCloseableIterator firstBatchIterator = source() - .read(config(), getConfiguredCatalog(), null); - final List dataFromFirstBatch = AutoCloseableIterators - .toListAndClose(firstBatchIterator); - final AirbyteStateMessage lastStateMessageFromFirstBatch = Iterables.getLast(extractStateMessages(dataFromFirstBatch)); - assertNotNull(lastStateMessageFromFirstBatch.getGlobal().getSharedState()); - assertNotNull(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state")); - assertNotNull(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state").get(IS_COMPRESSED)); - assertNotNull(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state").get(MYSQL_DB_HISTORY)); - assertNotNull(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state").get(MYSQL_CDC_OFFSET)); - assertTrue(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state").get(IS_COMPRESSED).asBoolean()); - - // INSERT records so that events are written to binlog and Debezium tries to parse them - final int recordsToCreate = 20; - // first batch of records. 20 created here and 6 created in setup method. - for (int recordsCreated = 0; recordsCreated < recordsToCreate; recordsCreated++) { - final JsonNode record = - Jsons.jsonNode(ImmutableMap - .of(COL_ID, 100 + recordsCreated, COL_MAKE_ID, 1, COL_MODEL, - "F-" + recordsCreated)); - writeModelRecord(record); - } - - final AutoCloseableIterator secondBatchIterator = source() - .read(config(), getConfiguredCatalog(), Jsons.jsonNode(Collections.singletonList(lastStateMessageFromFirstBatch))); - final List dataFromSecondBatch = AutoCloseableIterators - .toListAndClose(secondBatchIterator); - final AirbyteStateMessage lastStateMessageFromSecondBatch = Iterables.getLast(extractStateMessages(dataFromSecondBatch)); - assertNotNull(lastStateMessageFromSecondBatch.getGlobal().getSharedState()); - assertNotNull(lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state")); - assertNotNull(lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state").get(IS_COMPRESSED)); - assertNotNull(lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state").get(MYSQL_DB_HISTORY)); - assertNotNull(lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state").get(MYSQL_CDC_OFFSET)); - assertTrue(lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state").get(IS_COMPRESSED).asBoolean()); - - assertEquals(lastStateMessageFromFirstBatch.getGlobal().getSharedState().get("state").get(MYSQL_DB_HISTORY), - lastStateMessageFromSecondBatch.getGlobal().getSharedState().get("state").get(MYSQL_DB_HISTORY)); - - assertEquals(recordsToCreate, extractRecordMessages(dataFromSecondBatch).size()); - } - - private void writeDateRecords( - final JsonNode recordJson, - final String dbName, - final String streamName, - final String idCol, - final String dateCol) { - testdb.with("INSERT INTO `%s` .`%s` (%s, %s) VALUES (%s, %s);", dbName, streamName, - idCol, dateCol, - recordJson.get(idCol).asInt(), recordJson.get(dateCol).asText()); - } - - @Test - public void testInvalidDatetime_metaChangesPopulated() throws Exception { - final ConfiguredAirbyteCatalog configuredCatalog = Jsons.clone(getConfiguredCatalog()); - - // Add a datetime stream to the catalog - testdb - .withoutStrictMode() - .with(createTableSqlFmt(), getDatabaseName(), TEST_DATE_STREAM_NAME, - columnClause(ImmutableMap.of(COL_ID, "INTEGER", COL_DATE_TIME, "DATETIME"), Optional.of(COL_ID))); - - for (final JsonNode recordJson : DATE_TIME_RECORDS) { - writeDateRecords(recordJson, getDatabaseName(), TEST_DATE_STREAM_NAME, COL_ID, COL_DATE_TIME); - } - - final ConfiguredAirbyteStream airbyteStream = new ConfiguredAirbyteStream() - .withStream(CatalogHelpers.createAirbyteStream( - TEST_DATE_STREAM_NAME, - getDatabaseName(), - Field.of(COL_ID, JsonSchemaType.INTEGER), - Field.of(COL_DATE_TIME, JsonSchemaType.STRING_TIMESTAMP_WITHOUT_TIMEZONE)) - .withSupportedSyncModes( - Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of(COL_ID)))); - airbyteStream.setSyncMode(SyncMode.INCREMENTAL); - - final List streams = new ArrayList<>(); - streams.add(airbyteStream); - configuredCatalog.withStreams(streams); - - final AutoCloseableIterator read1 = source() - .read(config(), configuredCatalog, null); - final List actualRecords = AutoCloseableIterators.toListAndClose(read1); - - // Sync is expected to succeed with one record. However, the meta changes column should be populated - // for this record - // as it is an invalid date. As a result, this field will be omitted as Airbyte is unable to - // serialize the source value. - final Set recordMessages = extractRecordMessages(actualRecords); - assertEquals(recordMessages.size(), 1); - final AirbyteRecordMessage invalidDateRecord = recordMessages.stream().findFirst().get(); - - final AirbyteRecordMessageMetaChange expectedChange = - new AirbyteRecordMessageMetaChange().withReason(Reason.SOURCE_SERIALIZATION_ERROR).withChange( - Change.NULLED).withField(COL_DATE_TIME); - final AirbyteRecordMessageMeta expectedMessageMeta = new AirbyteRecordMessageMeta().withChanges(List.of(expectedChange)); - assertEquals(expectedMessageMeta, invalidDateRecord.getMeta()); - - ObjectMapper mapper = new ObjectMapper(); - final JsonNode expectedDataWithoutCdcFields = mapper.readTree("{\"id\":120, \"CAR_DATE\":null}"); - removeCDCColumns((ObjectNode) invalidDateRecord.getData()); - assertEquals(expectedDataWithoutCdcFields, invalidDateRecord.getData()); - } - - private void createTablesToIncreaseSchemaHistorySize() { - for (int i = 0; i <= 200; i++) { - final String tableName = generateRandomStringOf32Characters(); - final StringBuilder createTableQuery = new StringBuilder("CREATE TABLE " + tableName + "("); - String firstCol = null; - for (int j = 1; j <= 250; j++) { - final String columnName = generateRandomStringOf32Characters(); - if (j == 1) { - firstCol = columnName; - - } - createTableQuery.append(columnName).append(" INTEGER, "); - } - createTableQuery.append("PRIMARY KEY (").append(firstCol).append("));"); - testdb.with(createTableQuery.toString()); - } - } - - private static String generateRandomStringOf32Characters() { - final String characters = "abcdefghijklmnopqrstuvwxyz"; - final int length = 32; - - final StringBuilder randomString = new StringBuilder(length); - - for (int i = 0; i < length; i++) { - final int index = RANDOM.nextInt(characters.length()); - final char randomChar = characters.charAt(index); - randomString.append(randomChar); - } - - return randomString.toString(); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceWithSpecialDbNameTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceWithSpecialDbNameTest.java deleted file mode 100644 index 74dec8a9ce77..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CdcMysqlSourceWithSpecialDbNameTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import org.testcontainers.containers.MySQLContainer; - -public class CdcMysqlSourceWithSpecialDbNameTest extends CdcMysqlSourceTest { - - @Override - protected MySQLTestDatabase createTestDatabase() { - var container = new MySQLContainerFactory().shared( - BaseImage.MYSQL_8.reference, - ContainerModifier.INVALID_TIMEZONE_CEST.methodName, - ContainerModifier.CUSTOM_NAME.methodName); - return new TestDatabaseWithInvalidDatabaseName(container) - .initialized() - .withCdcPermissions(); - } - - static class TestDatabaseWithInvalidDatabaseName extends MySQLTestDatabase { - - public static final String INVALID_DB_NAME = "invalid@name"; - - public TestDatabaseWithInvalidDatabaseName(MySQLContainer container) { - super(container); - } - - @Override - public String getDatabaseName() { - return withNamespace(INVALID_DB_NAME); - } - - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CloudDeploymentMySqlSslTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CloudDeploymentMySqlSslTest.java deleted file mode 100644 index 9cc966b6b5df..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/CloudDeploymentMySqlSslTest.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.base.Source; -import io.airbyte.cdk.integrations.base.ssh.SshBastionContainer; -import io.airbyte.cdk.integrations.base.ssh.SshHelpers; -import io.airbyte.cdk.integrations.base.ssh.SshTunnel; -import io.airbyte.commons.features.EnvVariableFeatureFlags; -import io.airbyte.commons.features.FeatureFlagsWrapper; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.resources.MoreResources; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import java.util.concurrent.TimeUnit; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.Timeout; - -public class CloudDeploymentMySqlSslTest { - - private MySQLTestDatabase createTestDatabase(String... containerFactoryMethods) { - final var container = new MySQLContainerFactory().shared("mysql:8.0", containerFactoryMethods); - return new MySQLTestDatabase(container) - .withConnectionProperty("useSSL", "true") - .withConnectionProperty("requireSSL", "true") - .initialized(); - } - - private Source source() { - final var source = new MySqlSource(); - source.setFeatureFlags(FeatureFlagsWrapper.overridingDeploymentMode(new EnvVariableFeatureFlags(), "CLOUD")); - return MySqlSource.sshWrappedSource(source); - } - - @Test - void testSpec() throws Exception { - final ConnectorSpecification actual = source().spec(); - final ConnectorSpecification expected = - SshHelpers.injectSshIntoSpec(Jsons.deserialize(MoreResources.readResource("expected_cloud_spec.json"), ConnectorSpecification.class)); - assertEquals(expected, actual); - } - - @Test - void testStrictSSLUnsecuredNoTunnel() throws Exception { - try (final var testdb = createTestDatabase()) { - final var config = testdb.configBuilder() - .withHostAndPort() - .withDatabase() - .with(JdbcUtils.USERNAME_KEY, testdb.getUserName()) - .with(JdbcUtils.PASSWORD_KEY, "fake") - .with("tunnel_method", ImmutableMap.builder().put("tunnel_method", "NO_TUNNEL").build()) - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "preferred") - .build()) - .build(); - final AirbyteConnectionStatus actual = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, actual.getStatus()); - assertTrue(actual.getMessage().contains("Unsecured connection not allowed"), actual.getMessage()); - } - } - - @Test - void testStrictSSLSecuredNoTunnel() throws Exception { - final String PASSWORD = "Passw0rd"; - try (final var testdb = createTestDatabase("withRootAndServerCertificates", "withClientCertificate")) { - final var config = testdb.testConfigBuilder() - .with("tunnel_method", ImmutableMap.builder().put("tunnel_method", "NO_TUNNEL").build()) - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCertificates().caCertificate()) - .put("client_certificate", testdb.getCertificates().clientCertificate()) - .put("client_key", testdb.getCertificates().clientKey()) - .put("client_key_password", PASSWORD) - .build()) - .build(); - final AirbyteConnectionStatus actual = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, actual.getStatus()); - assertTrue(actual.getMessage().contains("Failed to create keystore for Client certificate"), actual.getMessage()); - } - } - - @Test - void testStrictSSLSecuredWithTunnel() throws Exception { - final String PASSWORD = "Passw0rd"; - try (final var testdb = createTestDatabase("withRootAndServerCertificates", "withClientCertificate")) { - final var config = testdb.configBuilder() - .withHostAndPort() - .withDatabase() - .with(JdbcUtils.USERNAME_KEY, testdb.getUserName()) - .with(JdbcUtils.PASSWORD_KEY, "fake") - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "verify_ca") - .put("ca_certificate", testdb.getCertificates().caCertificate()) - .put("client_certificate", testdb.getCertificates().clientCertificate()) - .put("client_key", testdb.getCertificates().clientKey()) - .put("client_key_password", PASSWORD) - .build()) - .with("tunnel_method", ImmutableMap.builder().put("tunnel_method", "SSH_KEY_AUTH").build()) - .build(); - final AirbyteConnectionStatus actual = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, actual.getStatus()); - assertTrue(actual.getMessage().contains("Could not connect with provided SSH configuration."), actual.getMessage()); - } - } - - @Test - void testStrictSSLUnsecuredWithTunnel() throws Exception { - try (final var testdb = createTestDatabase()) { - final var config = testdb.configBuilder() - .withHostAndPort() - .withDatabase() - .with(JdbcUtils.USERNAME_KEY, testdb.getUserName()) - .with(JdbcUtils.PASSWORD_KEY, "fake") - .withSsl(ImmutableMap.builder() - .put(JdbcUtils.MODE_KEY, "preferred") - .build()) - .with("tunnel_method", ImmutableMap.builder().put("tunnel_method", "SSH_KEY_AUTH").build()) - .build(); - final AirbyteConnectionStatus actual = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, actual.getStatus()); - assertTrue(actual.getMessage().contains("Could not connect with provided SSH configuration."), actual.getMessage()); - } - } - - @Test - @Timeout(value = 5, - unit = TimeUnit.MINUTES) - void testCheckWithSslModeDisabled() throws Exception { - try (final var testdb = createTestDatabase("withNetwork")) { - try (final SshBastionContainer bastion = new SshBastionContainer()) { - bastion.initAndStartBastion(testdb.getContainer().getNetwork()); - final var config = testdb.integrationTestConfigBuilder() - .with("tunnel_method", bastion.getTunnelMethod(SshTunnel.TunnelMethod.SSH_PASSWORD_AUTH, false)) - .withoutSsl() - .build(); - final AirbyteConnectionStatus actual = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.SUCCEEDED, actual.getStatus()); - } - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlDebugger.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlDebugger.java deleted file mode 100644 index 67c8f4d68687..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlDebugger.java +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ -package io.airbyte.integrations.source.mysql; - -import io.airbyte.cdk.integrations.debug.DebugUtil; - -public class MySqlDebugger { - - @SuppressWarnings({"unchecked", "deprecation", "resource"}) - public static void main(final String[] args) throws Exception { - final MySqlSource mysqlSource = new MySqlSource(); - DebugUtil.debug(mysqlSource); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlInitialLoadHandlerTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlInitialLoadHandlerTest.java deleted file mode 100644 index 7f09a54fcffb..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlInitialLoadHandlerTest.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import io.airbyte.integrations.source.mysql.MySqlQueryUtils.TableSizeInfo; -import io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadHandler; -import io.airbyte.protocol.models.AirbyteStreamNameNamespacePair; -import org.junit.jupiter.api.Test; - -public class MySqlInitialLoadHandlerTest { - - private static final long ONE_GB = 1_073_741_824; - private static final long ONE_MB = 1_048_576; - - @Test - void testInvalidOrNullTableSizeInfo() { - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair("table_name", "schema_name"); - assertEquals(MySqlInitialLoadHandler.calculateChunkSize(null, pair), 1_000_000L); - final TableSizeInfo invalidRowLengthInfo = new TableSizeInfo(ONE_GB, 0L); - assertEquals(MySqlInitialLoadHandler.calculateChunkSize(invalidRowLengthInfo, pair), 1_000_000L); - final TableSizeInfo invalidTableSizeInfo = new TableSizeInfo(0L, 0L); - assertEquals(MySqlInitialLoadHandler.calculateChunkSize(invalidTableSizeInfo, pair), 1_000_000L); - } - - @Test - void testTableSizeInfo() { - final AirbyteStreamNameNamespacePair pair = new AirbyteStreamNameNamespacePair("table_name", "schema_name"); - assertEquals(MySqlInitialLoadHandler.calculateChunkSize(new TableSizeInfo(ONE_GB, 2 * ONE_MB), pair), 512L); - assertEquals(MySqlInitialLoadHandler.calculateChunkSize(new TableSizeInfo(ONE_GB, 200L), pair), 5368709L); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java deleted file mode 100644 index d1ec0aacb680..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlJdbcSourceAcceptanceTest.java +++ /dev/null @@ -1,535 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -import static io.airbyte.cdk.integrations.debezium.DebeziumIteratorConstants.SYNC_CHECKPOINT_RECORDS_PROPERTY; -import static io.airbyte.integrations.source.mysql.initialsync.MySqlInitialLoadStateManager.STATE_TYPE_KEY; -import static java.util.stream.Collectors.toList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.source.jdbc.test.JdbcSourceAcceptanceTest; -import io.airbyte.cdk.integrations.source.relationaldb.models.DbStreamState; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.resources.MoreResources; -import io.airbyte.commons.util.MoreIterators; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.internal.models.CursorBasedStatus; -import io.airbyte.integrations.source.mysql.internal.models.InternalModels.StateType; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteCatalog; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; -import io.airbyte.protocol.models.v0.AirbyteMessage; -import io.airbyte.protocol.models.v0.AirbyteMessage.Type; -import io.airbyte.protocol.models.v0.AirbyteRecordMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage; -import io.airbyte.protocol.models.v0.AirbyteStateMessage.AirbyteStateType; -import io.airbyte.protocol.models.v0.AirbyteStateStats; -import io.airbyte.protocol.models.v0.AirbyteStream; -import io.airbyte.protocol.models.v0.AirbyteStreamState; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.ConnectorSpecification; -import io.airbyte.protocol.models.v0.DestinationSyncMode; -import io.airbyte.protocol.models.v0.StreamDescriptor; -import io.airbyte.protocol.models.v0.SyncMode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.junit.jupiter.api.Order; -import org.junit.jupiter.api.Test; - -@Order(2) -@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NULL_ON_SOME_PATH") -class MySqlJdbcSourceAcceptanceTest extends JdbcSourceAcceptanceTest { - - protected static final String USERNAME_WITHOUT_PERMISSION = "new_user"; - protected static final String PASSWORD_WITHOUT_PERMISSION = "new_password"; - - @Override - protected JsonNode config() { - return testdb.testConfigBuilder().build(); - } - - @Override - protected MySqlSource source() { - return new MySqlSource(); - } - - @Override - protected MySQLTestDatabase createTestDatabase() { - return MySQLTestDatabase.in(BaseImage.MYSQL_8); - } - - @Override - protected void maybeSetShorterConnectionTimeout(final JsonNode config) { - ((ObjectNode) config).put(JdbcUtils.JDBC_URL_PARAMS_KEY, "connectTimeout=1000"); - } - - // MySql does not support schemas in the way most dbs do. Instead we namespace by db name. - @Override - protected boolean supportsSchemas() { - return false; - } - - @Override - protected void validateFullRefreshStateMessageReadSuccess(final List stateMessages) { - var finalStateMessage = stateMessages.get(stateMessages.size() - 1); - assertEquals( - finalStateMessage.getStream().getStreamState().get("state_type").textValue(), - "primary_key"); - assertEquals(finalStateMessage.getStream().getStreamState().get("pk_name").textValue(), "id"); - assertEquals(finalStateMessage.getStream().getStreamState().get("pk_val").textValue(), "3"); - } - - @Test - @Override - protected void testReadMultipleTablesIncrementally() throws Exception { - final var config = config(); - ((ObjectNode) config).put(SYNC_CHECKPOINT_RECORDS_PROPERTY, 1); - final String streamOneName = TABLE_NAME + "one"; - // Create a fresh first table - testdb.with("CREATE TABLE %s (\n" - + " id int PRIMARY KEY,\n" - + " name VARCHAR(200) NOT NULL,\n" - + " updated_at VARCHAR(200) NOT NULL\n" - + ");", streamOneName) - .with("INSERT INTO %s(id, name, updated_at) VALUES (1,'picard', '2004-10-19')", - getFullyQualifiedTableName(streamOneName)) - .with("INSERT INTO %s(id, name, updated_at) VALUES (2, 'crusher', '2005-10-19')", - getFullyQualifiedTableName(streamOneName)) - .with("INSERT INTO %s(id, name, updated_at) VALUES (3, 'vash', '2006-10-19')", - getFullyQualifiedTableName(streamOneName)); - - // Create a fresh second table - final String streamTwoName = TABLE_NAME + "two"; - final String streamTwoFullyQualifiedName = getFullyQualifiedTableName(streamTwoName); - // Insert records into second table - testdb.with("CREATE TABLE %s (\n" - + " id int PRIMARY KEY,\n" - + " name VARCHAR(200) NOT NULL,\n" - + " updated_at DATE NOT NULL\n" - + ");", streamTwoName) - .with("INSERT INTO %s(id, name, updated_at) VALUES (40,'Jean Luc','2006-10-19')", - streamTwoFullyQualifiedName) - .with("INSERT INTO %s(id, name, updated_at) VALUES (41, 'Groot', '2006-10-19')", - streamTwoFullyQualifiedName) - .with("INSERT INTO %s(id, name, updated_at) VALUES (42, 'Thanos','2006-10-19')", - streamTwoFullyQualifiedName); - - // Create records list that we expect to see in the state message - final List streamTwoExpectedRecords = Arrays.asList( - createRecord(streamTwoName, getDefaultNamespace(), ImmutableMap.of( - COL_ID, 40, - COL_NAME, "Jean Luc", - COL_UPDATED_AT, "2006-10-19")), - createRecord(streamTwoName, getDefaultNamespace(), ImmutableMap.of( - COL_ID, 41, - COL_NAME, "Groot", - COL_UPDATED_AT, "2006-10-19")), - createRecord(streamTwoName, getDefaultNamespace(), ImmutableMap.of( - COL_ID, 42, - COL_NAME, "Thanos", - COL_UPDATED_AT, "2006-10-19"))); - - // Prep and create a configured catalog to perform sync - final AirbyteStream streamOne = getAirbyteStream(streamOneName, getDefaultNamespace()); - final AirbyteStream streamTwo = getAirbyteStream(streamTwoName, getDefaultNamespace()); - - final ConfiguredAirbyteCatalog configuredCatalog = CatalogHelpers.toDefaultConfiguredCatalog( - new AirbyteCatalog().withStreams(List.of(streamOne, streamTwo))); - configuredCatalog.getStreams().forEach(airbyteStream -> { - airbyteStream.setSyncMode(SyncMode.INCREMENTAL); - airbyteStream.setCursorField(List.of(COL_ID)); - airbyteStream.setDestinationSyncMode(DestinationSyncMode.APPEND); - airbyteStream.withPrimaryKey(List.of(List.of(COL_ID))); - }); - - // Perform initial sync - final List messagesFromFirstSync = MoreIterators - .toList(source().read(config, configuredCatalog, null)); - - final List recordsFromFirstSync = filterRecords(messagesFromFirstSync); - - setEmittedAtToNull(messagesFromFirstSync); - // All records in the 2 configured streams should be present - assertThat(filterRecords(recordsFromFirstSync)).containsExactlyElementsOf( - Stream.concat(getTestMessages(streamOneName).stream().parallel(), - streamTwoExpectedRecords.stream().parallel()).collect(toList())); - - final List actualFirstSyncState = extractStateMessage(messagesFromFirstSync); - // Since we are emitting a state message after each record, we should have 1 state for each record - - // 3 from stream1 and 3 from stream2 - assertEquals(6, actualFirstSyncState.size()); - - // The expected state type should be 2 primaryKey's and the last one being standard - final List expectedStateTypesFromFirstSync = List.of("primary_key", "primary_key", "cursor_based"); - final List stateTypeOfStreamOneStatesFromFirstSync = - extractSpecificFieldFromCombinedMessages(messagesFromFirstSync, streamOneName, STATE_TYPE_KEY); - final List stateTypeOfStreamTwoStatesFromFirstSync = - extractSpecificFieldFromCombinedMessages(messagesFromFirstSync, streamTwoName, STATE_TYPE_KEY); - // It should be the same for stream1 and stream2 - assertEquals(stateTypeOfStreamOneStatesFromFirstSync, expectedStateTypesFromFirstSync); - assertEquals(stateTypeOfStreamTwoStatesFromFirstSync, expectedStateTypesFromFirstSync); - - // Create the expected primaryKeys that we should see - final List expectedPrimaryKeysFromFirstSync = List.of("1", "2"); - final List primaryKeyFromStreamOneStatesFromFirstSync = - extractSpecificFieldFromCombinedMessages(messagesFromFirstSync, streamOneName, "pk_val"); - final List primaryKeyFromStreamTwoStatesFromFirstSync = - extractSpecificFieldFromCombinedMessages(messagesFromFirstSync, streamOneName, "pk_val"); - - // Verifying each element and its index to match. - // Only checking the first 2 elements since we have verified that the last state_type is - // "cursor_based" - assertEquals(primaryKeyFromStreamOneStatesFromFirstSync.get(0), expectedPrimaryKeysFromFirstSync.get(0)); - assertEquals(primaryKeyFromStreamOneStatesFromFirstSync.get(1), expectedPrimaryKeysFromFirstSync.get(1)); - assertEquals(primaryKeyFromStreamTwoStatesFromFirstSync.get(0), expectedPrimaryKeysFromFirstSync.get(0)); - assertEquals(primaryKeyFromStreamTwoStatesFromFirstSync.get(1), expectedPrimaryKeysFromFirstSync.get(1)); - - // Extract only state messages for each stream - final List streamOneStateMessagesFromFirstSync = extractStateMessage(messagesFromFirstSync, streamOneName); - final List streamTwoStateMessagesFromFirstSync = extractStateMessage(messagesFromFirstSync, streamTwoName); - - // Extract the incremental states of each stream's first and second state message - final List streamOneIncrementalStatesFromFirstSync = - List.of(streamOneStateMessagesFromFirstSync.get(0).getStream().getStreamState().get("incremental_state"), - streamOneStateMessagesFromFirstSync.get(1).getStream().getStreamState().get("incremental_state")); - final JsonNode streamOneFinalStreamStateFromFirstSync = streamOneStateMessagesFromFirstSync.get(2).getStream().getStreamState(); - - final List streamTwoIncrementalStatesFromFirstSync = - List.of(streamTwoStateMessagesFromFirstSync.get(0).getStream().getStreamState().get("incremental_state"), - streamTwoStateMessagesFromFirstSync.get(1).getStream().getStreamState().get("incremental_state")); - final JsonNode streamTwoFinalStreamStateFromFirstSync = streamTwoStateMessagesFromFirstSync.get(2).getStream().getStreamState(); - - // The incremental_state of each stream's first and second incremental states is expected - // to be identical to the stream_state of the final state message for each stream - assertEquals(streamOneIncrementalStatesFromFirstSync.get(0), streamOneFinalStreamStateFromFirstSync); - assertEquals(streamOneIncrementalStatesFromFirstSync.get(1), streamOneFinalStreamStateFromFirstSync); - assertEquals(streamTwoIncrementalStatesFromFirstSync.get(0), streamTwoFinalStreamStateFromFirstSync); - assertEquals(streamTwoIncrementalStatesFromFirstSync.get(1), streamTwoFinalStreamStateFromFirstSync); - - // Sync should work with a primaryKey state AND a cursor-based state from each stream - // Forcing a sync with - // - stream one state still being the first record read via Primary Key. - // - stream two state being the Primary Key state before the final emitted state before the cursor - // switch - final List messagesFromSecondSyncWithMixedStates = MoreIterators - .toList(source().read(config, configuredCatalog, - Jsons.jsonNode(List.of(streamOneStateMessagesFromFirstSync.get(0), - streamTwoStateMessagesFromFirstSync.get(1))))); - - // Extract only state messages for each stream after second sync - final List streamOneStateMessagesFromSecondSync = - extractStateMessage(messagesFromSecondSyncWithMixedStates, streamOneName); - final List stateTypeOfStreamOneStatesFromSecondSync = - extractSpecificFieldFromCombinedMessages(messagesFromSecondSyncWithMixedStates, streamOneName, STATE_TYPE_KEY); - - final List streamTwoStateMessagesFromSecondSync = - extractStateMessage(messagesFromSecondSyncWithMixedStates, streamTwoName); - final List stateTypeOfStreamTwoStatesFromSecondSync = - extractSpecificFieldFromCombinedMessages(messagesFromSecondSyncWithMixedStates, streamTwoName, STATE_TYPE_KEY); - - // Stream One states after the second sync are expected to have 2 stream states - // - 1 with PrimaryKey state_type and 1 state that is of cursorBased state type - assertEquals(2, streamOneStateMessagesFromSecondSync.size()); - assertEquals(List.of("primary_key", "cursor_based"), stateTypeOfStreamOneStatesFromSecondSync); - - // Stream Two states after the second sync are expected to have 1 stream state - // - The state that is of cursorBased state type - assertEquals(1, streamTwoStateMessagesFromSecondSync.size()); - assertEquals(List.of("cursor_based"), stateTypeOfStreamTwoStatesFromSecondSync); - - // Add some data to each table and perform a third read. - // Expect to see all records be synced via cursorBased method and not primaryKey - testdb.with("INSERT INTO %s(id, name, updated_at) VALUES (4,'Hooper','2006-10-19')", - getFullyQualifiedTableName(streamOneName)) - .with("INSERT INTO %s(id, name, updated_at) VALUES (43, 'Iron Man', '2006-10-19')", - streamTwoFullyQualifiedName); - - final List messagesFromThirdSync = MoreIterators - .toList(source().read(config, configuredCatalog, - Jsons.jsonNode(List.of(streamOneStateMessagesFromSecondSync.get(1), - streamTwoStateMessagesFromSecondSync.get(0))))); - - // Extract only state messages, state type, and cursor for each stream after second sync - final List streamOneStateMessagesFromThirdSync = - extractStateMessage(messagesFromThirdSync, streamOneName); - final List stateTypeOfStreamOneStatesFromThirdSync = - extractSpecificFieldFromCombinedMessages(messagesFromThirdSync, streamOneName, STATE_TYPE_KEY); - final List cursorOfStreamOneStatesFromThirdSync = - extractSpecificFieldFromCombinedMessages(messagesFromThirdSync, streamOneName, "cursor"); - - final List streamTwoStateMessagesFromThirdSync = - extractStateMessage(messagesFromThirdSync, streamTwoName); - final List stateTypeOfStreamTwoStatesFromThirdSync = - extractSpecificFieldFromCombinedMessages(messagesFromThirdSync, streamTwoName, STATE_TYPE_KEY); - final List cursorOfStreamTwoStatesFromThirdSync = - extractSpecificFieldFromCombinedMessages(messagesFromThirdSync, streamTwoName, "cursor"); - - // Both streams should now be synced via standard cursor and have updated max cursor values - // cursor: 4 for stream one - // cursor: 43 for stream two - assertEquals(1, streamOneStateMessagesFromThirdSync.size()); - assertEquals(List.of("cursor_based"), stateTypeOfStreamOneStatesFromThirdSync); - assertEquals(List.of("4"), cursorOfStreamOneStatesFromThirdSync); - - assertEquals(1, streamTwoStateMessagesFromThirdSync.size()); - assertEquals(List.of("cursor_based"), stateTypeOfStreamTwoStatesFromThirdSync); - assertEquals(List.of("43"), cursorOfStreamTwoStatesFromThirdSync); - } - - @Test - public void testSpec() throws Exception { - final ConnectorSpecification actual = source().spec(); - final ConnectorSpecification expected = Jsons.deserialize(MoreResources.readResource("spec.json"), ConnectorSpecification.class); - - assertEquals(expected, actual); - } - - /** - * MySQL Error Codes: - *

- * https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-error-sqlstates.html - *

- * - * @throws Exception - */ - @Test - void testCheckIncorrectPasswordFailure() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - ((ObjectNode) config).put(JdbcUtils.PASSWORD_KEY, "fake"); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 08001;"), status.getMessage()); - } - - @Test - public void testCheckIncorrectUsernameFailure() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - ((ObjectNode) config).put(JdbcUtils.USERNAME_KEY, "fake"); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - // do not test for message since there seems to be flakiness where sometimes the test will get the - // message with - // State code: 08001 or State code: 28000 - } - - @Test - public void testCheckIncorrectHostFailure() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - ((ObjectNode) config).put(JdbcUtils.HOST_KEY, "localhost2"); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 08S01;"), status.getMessage()); - } - - @Test - public void testCheckIncorrectPortFailure() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - ((ObjectNode) config).put(JdbcUtils.PORT_KEY, "0000"); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 08S01;"), status.getMessage()); - } - - @Test - public void testCheckIncorrectDataBaseFailure() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - ((ObjectNode) config).put(JdbcUtils.DATABASE_KEY, "wrongdatabase"); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 42000; Error code: 1049;"), status.getMessage()); - } - - @Test - public void testUserHasNoPermissionToDataBase() throws Exception { - final var config = config(); - maybeSetShorterConnectionTimeout(config); - final String usernameWithoutPermission = testdb.withNamespace(USERNAME_WITHOUT_PERMISSION); - testdb.with("CREATE USER '%s'@'%%' IDENTIFIED BY '%s';", usernameWithoutPermission, PASSWORD_WITHOUT_PERMISSION); - ((ObjectNode) config).put(JdbcUtils.USERNAME_KEY, usernameWithoutPermission); - ((ObjectNode) config).put(JdbcUtils.PASSWORD_KEY, PASSWORD_WITHOUT_PERMISSION); - final AirbyteConnectionStatus status = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.FAILED, status.getStatus()); - assertTrue(status.getMessage().contains("State code: 08001;"), status.getMessage()); - } - - @Test - public void testFullRefresh() throws Exception { - - } - - @Override - protected DbStreamState buildStreamState(final ConfiguredAirbyteStream configuredAirbyteStream, - final String cursorField, - final String cursorValue) { - return new CursorBasedStatus().withStateType(StateType.CURSOR_BASED).withVersion(2L) - .withStreamName(configuredAirbyteStream.getStream().getName()) - .withStreamNamespace(configuredAirbyteStream.getStream().getNamespace()) - .withCursorField(List.of(cursorField)) - .withCursor(cursorValue) - .withCursorRecordCount(1L); - } - - @Override - protected List getExpectedAirbyteMessagesSecondSync(final String namespace) { - final List expectedMessages = new ArrayList<>(); - expectedMessages.add(new AirbyteMessage().withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage().withStream(streamName()).withNamespace(namespace) - .withData(Jsons.jsonNode(ImmutableMap - .of(COL_ID, ID_VALUE_4, - COL_NAME, "riker", - COL_UPDATED_AT, "2006-10-19"))))); - expectedMessages.add(new AirbyteMessage().withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage().withStream(streamName()).withNamespace(namespace) - .withData(Jsons.jsonNode(ImmutableMap - .of(COL_ID, ID_VALUE_5, - COL_NAME, "data", - COL_UPDATED_AT, "2006-10-19"))))); - final DbStreamState state = new CursorBasedStatus() - .withStateType(StateType.CURSOR_BASED) - .withVersion(2L) - .withStreamName(streamName()) - .withStreamNamespace(namespace) - .withCursorField(ImmutableList.of(COL_ID)) - .withCursor("5") - .withCursorRecordCount(1L); - - expectedMessages.addAll(createExpectedTestMessages(List.of(state), 2L)); - return expectedMessages; - } - - @Override - protected List getTestMessages() { - return getTestMessages(streamName()); - } - - protected List getTestMessages(final String streamName) { - return List.of( - new AirbyteMessage().withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage().withStream(streamName).withNamespace(getDefaultNamespace()) - .withData(Jsons.jsonNode(Map - .of(COL_ID, ID_VALUE_1, - COL_NAME, "picard", - COL_UPDATED_AT, "2004-10-19")))), - new AirbyteMessage().withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage().withStream(streamName).withNamespace(getDefaultNamespace()) - .withData(Jsons.jsonNode(Map - .of(COL_ID, ID_VALUE_2, - COL_NAME, "crusher", - COL_UPDATED_AT, - "2005-10-19")))), - new AirbyteMessage().withType(Type.RECORD) - .withRecord(new AirbyteRecordMessage().withStream(streamName).withNamespace(getDefaultNamespace()) - .withData(Jsons.jsonNode(Map - .of(COL_ID, ID_VALUE_3, - COL_NAME, "vash", - COL_UPDATED_AT, "2006-10-19"))))); - } - - private AirbyteStream getAirbyteStream(final String tableName, final String namespace) { - return CatalogHelpers.createAirbyteStream( - tableName, - namespace, - Field.of(COL_ID, JsonSchemaType.INTEGER), - Field.of(COL_NAME, JsonSchemaType.STRING), - Field.of(COL_UPDATED_AT, JsonSchemaType.STRING_DATE)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of(COL_ID))); - } - - @Override - protected AirbyteCatalog getCatalog(final String defaultNamespace) { - return new AirbyteCatalog().withStreams(Lists.newArrayList( - CatalogHelpers.createAirbyteStream( - TABLE_NAME, - defaultNamespace, - Field.of(COL_ID, JsonSchemaType.INTEGER), - Field.of(COL_NAME, JsonSchemaType.STRING), - Field.of(COL_UPDATED_AT, JsonSchemaType.STRING_DATE)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of(COL_ID))) - .withIsResumable(true), - CatalogHelpers.createAirbyteStream( - TABLE_NAME_WITHOUT_PK, - defaultNamespace, - Field.of(COL_ID, JsonSchemaType.INTEGER), - Field.of(COL_NAME, JsonSchemaType.STRING), - Field.of(COL_UPDATED_AT, JsonSchemaType.STRING_DATE)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(Collections.emptyList()) - .withIsResumable(false), - CatalogHelpers.createAirbyteStream( - TABLE_NAME_COMPOSITE_PK, - defaultNamespace, - Field.of(COL_FIRST_NAME, JsonSchemaType.STRING), - Field.of(COL_LAST_NAME, JsonSchemaType.STRING), - Field.of(COL_UPDATED_AT, JsonSchemaType.STRING_DATE)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey( - List.of(List.of(COL_FIRST_NAME), List.of(COL_LAST_NAME))) - .withIsResumable(true))); - } - - // Override from parent class as we're no longer including the legacy Data field. - @Override - protected List createExpectedTestMessages(final List states, final long numRecords) { - return states.stream() - .map(s -> new AirbyteMessage().withType(Type.STATE) - .withState( - new AirbyteStateMessage().withType(AirbyteStateType.STREAM) - .withStream(new AirbyteStreamState() - .withStreamDescriptor(new StreamDescriptor().withNamespace(s.getStreamNamespace()).withName(s.getStreamName())) - .withStreamState(Jsons.jsonNode(s))) - .withSourceStats(new AirbyteStateStats().withRecordCount((double) numRecords)))) - .collect( - Collectors.toList()); - } - - @Override - protected List createState(final List states) { - return states.stream() - .map(s -> new AirbyteStateMessage().withType(AirbyteStateType.STREAM) - .withStream(new AirbyteStreamState() - .withStreamDescriptor(new StreamDescriptor().withNamespace(s.getStreamNamespace()).withName(s.getStreamName())) - .withStreamState(Jsons.jsonNode(s)))) - .collect( - Collectors.toList()); - } - - @Override - protected JsonNode getStateData(final AirbyteMessage airbyteMessage, final String streamName) { - final JsonNode streamState = airbyteMessage.getState().getStream().getStreamState(); - if (streamState.get("stream_name").asText().equals(streamName)) { - return streamState; - } - - throw new IllegalArgumentException("Stream not found in state message: " + streamName); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceOperationsTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceOperationsTest.java deleted file mode 100644 index b989e2163c84..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceOperationsTest.java +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsInAnyOrder; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.jdbc.DateTimeConverter; -import io.airbyte.commons.json.Jsons; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; -import java.util.function.IntFunction; -import org.junit.jupiter.api.Test; - -public class MySqlSourceOperationsTest { - - @Test - public void dateColumnAsCursor() throws SQLException { - testImpl( - "DATE", - i -> LocalDate.of(2019, 1, i), - DateTimeConverter::convertToDate, - LocalDate::toString, - MysqlType.DATE, - DateTimeConverter.convertToDate(LocalDate.of(2019, 1, 1)), - "2019-01-01T00:00:00Z"); - } - - @Test - public void timeColumnAsCursor() throws SQLException { - testImpl( - "TIME", - i -> LocalTime.of(20, i, 0), - DateTimeConverter::convertToTime, - LocalTime::toString, - MysqlType.TIME, - DateTimeConverter.convertToTime(LocalTime.of(20, 1, 0)), - "1970-01-01T20:01:00Z"); - } - - @Test - public void dateTimeColumnAsCursor() throws SQLException { - testImpl( - "DATETIME", - i -> LocalDateTime.of(2019, i, 20, 3, 0, 0), - DateTimeConverter::convertToTimestamp, - LocalDateTime::toString, - MysqlType.DATETIME, - DateTimeConverter.convertToTimestamp(LocalDateTime.of(2019, 1, 20, 3, 0, 0)), - "2019-01-20T03:00:00.000000"); - } - - @Test - public void timestampColumnAsCursor() throws SQLException { - testImpl( - "TIMESTAMP", - i -> Instant.ofEpochSecond(1660298508L).plusSeconds(i - 1), - DateTimeConverter::convertToTimestampWithTimezone, - r -> Timestamp.from(r).toString(), - MysqlType.TIMESTAMP, - DateTimeConverter.convertToTimestampWithTimezone(Instant.ofEpochSecond(1660298508L)), - Instant.ofEpochSecond(1660298508L).toString()); - } - - private void testImpl( - final String sqlType, - IntFunction recordBuilder, - Function airbyteRecordStringifier, - Function sqlRecordStringifier, - MysqlType mysqlType, - String initialCursorFieldValue, - // Test to check backward compatibility for connectors created before PR - // https://github.com/airbytehq/airbyte/pull/15504 - String backwardCompatibleInitialCursorFieldValue) - throws SQLException { - final var sqlSourceOperations = new MySqlSourceOperations(); - final String cursorColumn = "cursor_column"; - try (final var testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8) - .with("CREATE TABLE cursor_table (id INTEGER PRIMARY KEY, %s %s);", cursorColumn, sqlType)) { - - final List expectedRecords = new ArrayList<>(); - for (int i = 1; i <= 4; i++) { - final ObjectNode jsonNode = (ObjectNode) Jsons.jsonNode(Collections.emptyMap()); - jsonNode.put("id", i); - final T cursorValue = recordBuilder.apply(i); - jsonNode.put("cursor_column", airbyteRecordStringifier.apply(cursorValue)); - testdb.with("INSERT INTO cursor_table VALUES (%d, '%s');", i, sqlRecordStringifier.apply(cursorValue)); - if (i >= 2) { - expectedRecords.add(jsonNode); - } - } - - try (final Connection connection = testdb.getContainer().createConnection("")) { - final PreparedStatement preparedStatement = connection.prepareStatement( - "SELECT * FROM " + testdb.getDatabaseName() + ".cursor_table WHERE " + cursorColumn + " > ?"); - for (final var initialValue : List.of(initialCursorFieldValue, backwardCompatibleInitialCursorFieldValue)) { - sqlSourceOperations.setCursorField(preparedStatement, 1, mysqlType, initialValue); - final List actualRecords = new ArrayList<>(); - try (final ResultSet resultSet = preparedStatement.executeQuery()) { - while (resultSet.next()) { - final ObjectNode jsonNode = (ObjectNode) Jsons.jsonNode(Collections.emptyMap()); - for (int i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) { - sqlSourceOperations.copyToJsonField(resultSet, i, jsonNode); - } - actualRecords.add(jsonNode); - } - } - assertThat(actualRecords, containsInAnyOrder(expectedRecords.toArray())); - } - } - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceTests.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceTests.java deleted file mode 100644 index ef8bb7646677..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSourceTests.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.source.jdbc.AbstractJdbcSource; -import io.airbyte.cdk.integrations.source.jdbc.AbstractJdbcSource.PrimaryKeyAttributesFromDb; -import io.airbyte.commons.exceptions.ConfigErrorException; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.util.MoreIterators; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.BaseImage; -import io.airbyte.integrations.source.mysql.MySQLTestDatabase.ContainerModifier; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteConnectionStatus; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteStream; -import io.airbyte.protocol.models.v0.DestinationSyncMode; -import io.airbyte.protocol.models.v0.SyncMode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -public class MySqlSourceTests { - - public MySqlSource source() { - return new MySqlSource(); - } - - @Test - public void testSettingTimezones() throws Exception { - try (final var testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8, ContainerModifier.MOSCOW_TIMEZONE)) { - final var config = testdb.testConfigBuilder() - .with(JdbcUtils.JDBC_URL_PARAMS_KEY, "serverTimezone=Europe/Moscow") - .withoutSsl() - .build(); - final AirbyteConnectionStatus check = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.SUCCEEDED, check.getStatus(), check.getMessage()); - } - } - - @Test - void testJdbcUrlWithEscapedDatabaseName() { - final JsonNode jdbcConfig = source().toDatabaseConfig(buildConfigEscapingNeeded()); - assertNotNull(jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText()); - assertTrue(jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText().startsWith(EXPECTED_JDBC_ESCAPED_URL)); - } - - private static final String EXPECTED_JDBC_ESCAPED_URL = "jdbc:mysql://localhost:1111/db%2Ffoo?"; - - private JsonNode buildConfigEscapingNeeded() { - return Jsons.jsonNode(ImmutableMap.of( - JdbcUtils.HOST_KEY, "localhost", - JdbcUtils.PORT_KEY, 1111, - JdbcUtils.USERNAME_KEY, "user", - JdbcUtils.DATABASE_KEY, "db/foo")); - } - - @Test - @Disabled("See https://github.com/airbytehq/airbyte/pull/23908#issuecomment-1463753684, enable once communication is out") - public void testNullCursorValueShouldThrowException() { - try (final var testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8) - .with("CREATE TABLE null_cursor_table(id INTEGER NULL);") - .with("INSERT INTO null_cursor_table(id) VALUES (1), (2), (NULL);") - .with("CREATE VIEW null_cursor_view(id) AS SELECT null_cursor_table.id FROM null_cursor_table;")) { - final var config = testdb.testConfigBuilder().withoutSsl().build(); - - final var tableStream = new ConfiguredAirbyteStream() - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withSyncMode(SyncMode.INCREMENTAL) - .withStream(CatalogHelpers.createAirbyteStream( - "null_cursor_table", - testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of("id")))); - final var tableCatalog = new ConfiguredAirbyteCatalog().withStreams(List.of(tableStream)); - final var tableThrowable = catchThrowable(() -> MoreIterators.toSet(source().read(config, tableCatalog, null))); - assertThat(tableThrowable).isInstanceOf(ConfigErrorException.class).hasMessageContaining(NULL_CURSOR_EXCEPTION_MESSAGE_CONTAINS); - - final var viewStream = new ConfiguredAirbyteStream() - .withCursorField(Lists.newArrayList("id")) - .withDestinationSyncMode(DestinationSyncMode.APPEND) - .withSyncMode(SyncMode.INCREMENTAL) - .withStream(CatalogHelpers.createAirbyteStream( - "null_cursor_view", - testdb.getDatabaseName(), - Field.of("id", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of("id")))); - final var viewCatalog = new ConfiguredAirbyteCatalog().withStreams(List.of(viewStream)); - final var viewThrowable = catchThrowable(() -> MoreIterators.toSet(source().read(config, viewCatalog, null))); - assertThat(viewThrowable).isInstanceOf(ConfigErrorException.class).hasMessageContaining(NULL_CURSOR_EXCEPTION_MESSAGE_CONTAINS); - } - } - - static private final String NULL_CURSOR_EXCEPTION_MESSAGE_CONTAINS = "The following tables have invalid columns " + - "selected as cursor, please select a column with a well-defined ordering with no null values as a cursor."; - - @Test - void testParseJdbcParameters() { - Map parameters = - MySqlSource.parseJdbcParameters("theAnswerToLiveAndEverything=42&sessionVariables=max_execution_time=10000&foo=bar", "&"); - assertEquals("max_execution_time=10000", parameters.get("sessionVariables")); - assertEquals("42", parameters.get("theAnswerToLiveAndEverything")); - assertEquals("bar", parameters.get("foo")); - } - - @Test - public void testJDBCSessionVariable() throws Exception { - try (final var testdb = MySQLTestDatabase.in(BaseImage.MYSQL_8)) { - final var config = testdb.testConfigBuilder() - .with(JdbcUtils.JDBC_URL_PARAMS_KEY, "sessionVariables=MAX_EXECUTION_TIME=28800000") - .withoutSsl() - .build(); - final AirbyteConnectionStatus check = source().check(config); - assertEquals(AirbyteConnectionStatus.Status.SUCCEEDED, check.getStatus()); - } - } - - @Test - public void testPrimaryKeyOrder() { - final List primaryKeys = new ArrayList<>(); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-a", "col1", 3)); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-b", "xcol1", 3)); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-a", "col2", 2)); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-b", "xcol2", 2)); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-a", "col3", 1)); - primaryKeys.add(new PrimaryKeyAttributesFromDb("stream-b", "xcol3", 1)); - - final Map> result = AbstractJdbcSource.aggregatePrimateKeys(primaryKeys); - assertEquals(2, result.size()); - assertTrue(result.containsKey("stream-a")); - assertEquals(3, result.get("stream-a").size()); - assertEquals(Arrays.asList("col3", "col2", "col1"), result.get("stream-a")); - - assertTrue(result.containsKey("stream-b")); - assertEquals(3, result.get("stream-b").size()); - assertEquals(Arrays.asList("xcol3", "xcol2", "xcol1"), result.get("stream-b")); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSslJdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSslJdbcSourceAcceptanceTest.java deleted file mode 100644 index bf47dd4da9bb..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlSslJdbcSourceAcceptanceTest.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import com.fasterxml.jackson.databind.JsonNode; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import org.junit.jupiter.api.Order; - -@Order(3) -@edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "NP_NULL_ON_SOME_PATH") -class MySqlSslJdbcSourceAcceptanceTest extends MySqlJdbcSourceAcceptanceTest { - - @Override - protected JsonNode config() { - return testdb.testConfigBuilder() - .with(JdbcUtils.SSL_KEY, true) - .build(); - } - - @Override - protected MySQLTestDatabase createTestDatabase() { - return new MySQLTestDatabase(new MySQLContainerFactory().shared("mysql:8.0")) - .withConnectionProperty("useSSL", "true") - .withConnectionProperty("requireSSL", "true") - .initialized() - .with("SHOW STATUS LIKE 'Ssl_cipher'"); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlStressTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlStressTest.java deleted file mode 100644 index 66fba410c3c0..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MySqlStressTest.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import com.mysql.cj.MysqlType; -import io.airbyte.cdk.db.Database; -import io.airbyte.cdk.db.factory.DSLContextFactory; -import io.airbyte.cdk.db.factory.DatabaseDriver; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.cdk.integrations.source.jdbc.AbstractJdbcSource; -import io.airbyte.cdk.integrations.source.jdbc.test.JdbcStressTest; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.string.Strings; -import java.sql.Connection; -import java.sql.DriverManager; -import java.util.Optional; -import java.util.concurrent.Callable; -import org.jooq.DSLContext; -import org.jooq.SQLDialect; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.testcontainers.containers.MySQLContainer; - -@Disabled -class MySqlStressTest extends JdbcStressTest { - - private static final String TEST_USER = "test"; - private static final Callable TEST_PASSWORD = () -> "test"; - private static MySQLContainer container; - - private JsonNode config; - private Database database; - private DSLContext dslContext; - - @BeforeAll - static void init() throws Exception { - container = new MySQLContainer<>("mysql:8.0") - .withUsername(TEST_USER) - .withPassword(TEST_PASSWORD.call()) - .withEnv("MYSQL_ROOT_HOST", "%") - .withEnv("MYSQL_ROOT_PASSWORD", TEST_PASSWORD.call()); - container.start(); - final Connection connection = DriverManager.getConnection(container.getJdbcUrl(), "root", TEST_PASSWORD.call()); - connection.createStatement().execute("GRANT ALL PRIVILEGES ON *.* TO '" + TEST_USER + "'@'%';\n"); - } - - @BeforeEach - public void setup() throws Exception { - config = Jsons.jsonNode(ImmutableMap.builder() - .put(JdbcUtils.HOST_KEY, container.getHost()) - .put(JdbcUtils.PORT_KEY, container.getFirstMappedPort()) - .put(JdbcUtils.DATABASE_KEY, Strings.addRandomSuffix("db", "_", 10)) - .put(JdbcUtils.USERNAME_KEY, TEST_USER) - .put(JdbcUtils.PASSWORD_KEY, TEST_PASSWORD.call()) - .build()); - - dslContext = DSLContextFactory.create( - config.get(JdbcUtils.USERNAME_KEY).asText(), - config.get(JdbcUtils.PASSWORD_KEY).asText(), - DatabaseDriver.MYSQL.getDriverClassName(), - String.format("jdbc:mysql://%s:%s", - config.get(JdbcUtils.HOST_KEY).asText(), - config.get(JdbcUtils.PORT_KEY).asText()), - SQLDialect.MYSQL); - database = new Database(dslContext); - - database.query(ctx -> { - ctx.fetch("CREATE DATABASE " + config.get(JdbcUtils.DATABASE_KEY).asText()); - return null; - }); - - super.setup(); - } - - @AfterAll - static void cleanUp() { - container.close(); - } - - // MySql does not support schemas in the way most dbs do. Instead we namespace by db name. - @Override - public Optional getDefaultSchemaName() { - return Optional.of(config.get(JdbcUtils.DATABASE_KEY).asText()); - } - - @Override - public AbstractJdbcSource getSource() { - return new MySqlSource(); - } - - @Override - public String getDriverClass() { - return MySqlSource.DRIVER_CLASS; - } - - @Override - public JsonNode getConfig() { - return Jsons.clone(config); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MysqlDebeziumStateUtilTest.java b/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MysqlDebeziumStateUtilTest.java deleted file mode 100644 index 284fdaa300f0..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/java/io/airbyte/integrations/source/mysql/MysqlDebeziumStateUtilTest.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import com.fasterxml.jackson.databind.JsonNode; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import io.airbyte.cdk.db.Database; -import io.airbyte.cdk.db.factory.DSLContextFactory; -import io.airbyte.cdk.db.factory.DataSourceFactory; -import io.airbyte.cdk.db.factory.DatabaseDriver; -import io.airbyte.cdk.db.jdbc.DefaultJdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcDatabase; -import io.airbyte.cdk.db.jdbc.JdbcUtils; -import io.airbyte.commons.json.Jsons; -import io.airbyte.commons.string.Strings; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil; -import io.airbyte.integrations.source.mysql.cdc.MySqlDebeziumStateUtil.MysqlDebeziumStateAttributes; -import io.airbyte.protocol.models.Field; -import io.airbyte.protocol.models.JsonSchemaType; -import io.airbyte.protocol.models.v0.AirbyteCatalog; -import io.airbyte.protocol.models.v0.CatalogHelpers; -import io.airbyte.protocol.models.v0.ConfiguredAirbyteCatalog; -import io.airbyte.protocol.models.v0.SyncMode; -import java.sql.SQLException; -import java.time.Instant; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import org.jooq.SQLDialect; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.testcontainers.containers.MySQLContainer; - -public class MysqlDebeziumStateUtilTest { - - private static final String DB_NAME = Strings.addRandomSuffix("db", "_", 10).toLowerCase(); - private static final String TABLE_NAME = Strings.addRandomSuffix("table", "_", 10).toLowerCase(); - private static final Properties MYSQL_PROPERTIES = new Properties(); - private static final String DB_CREATE_QUERY = "CREATE DATABASE " + DB_NAME; - private static final String TABLE_CREATE_QUERY = "CREATE TABLE " + DB_NAME + "." + TABLE_NAME + " (id INTEGER, name VARCHAR(200), PRIMARY KEY(id))"; - private static final AirbyteCatalog CATALOG = new AirbyteCatalog().withStreams(List.of( - CatalogHelpers.createAirbyteStream( - TABLE_NAME, - DB_NAME, - Field.of("id", JsonSchemaType.INTEGER), - Field.of("string", JsonSchemaType.STRING)) - .withSupportedSyncModes(Lists.newArrayList(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL)) - .withSourceDefinedPrimaryKey(List.of(List.of("id"))))); - protected static final ConfiguredAirbyteCatalog CONFIGURED_CATALOG = CatalogHelpers.toDefaultConfiguredCatalog(CATALOG); - - static { - CONFIGURED_CATALOG.getStreams().forEach(s -> s.setSyncMode(SyncMode.INCREMENTAL)); - MYSQL_PROPERTIES.setProperty("connector.class", "io.debezium.connector.mysql.MySqlConnector"); - MYSQL_PROPERTIES.setProperty("database.server.id", "5000"); - } - - @Test - public void debeziumInitialStateConstructTest() throws SQLException { - try (final MySQLContainer container = new MySQLContainer<>("mysql:8.0")) { - container.start(); - initDB(container); - final JdbcDatabase database = getJdbcDatabase(container); - final MySqlDebeziumStateUtil mySqlDebeziumStateUtil = new MySqlDebeziumStateUtil(); - final JsonNode debeziumState = mySqlDebeziumStateUtil.constructInitialDebeziumState(MYSQL_PROPERTIES, CONFIGURED_CATALOG, database); - Assertions.assertEquals(3, Jsons.object(debeziumState, Map.class).size()); - Assertions.assertTrue(debeziumState.has("is_compressed")); - Assertions.assertFalse(debeziumState.get("is_compressed").asBoolean()); - Assertions.assertTrue(debeziumState.has("mysql_db_history")); - Assertions.assertNotNull(debeziumState.get("mysql_db_history")); - Assertions.assertTrue(debeziumState.has("mysql_cdc_offset")); - final Map mysqlCdcOffset = Jsons.object(debeziumState.get("mysql_cdc_offset"), Map.class); - Assertions.assertEquals(1, mysqlCdcOffset.size()); - Assertions.assertTrue(mysqlCdcOffset.containsKey("[\"" + DB_NAME + "\",{\"server\":\"" + DB_NAME + "\"}]")); - Assertions.assertNotNull(mysqlCdcOffset.get("[\"" + DB_NAME + "\",{\"server\":\"" + DB_NAME + "\"}]")); - - final Optional parsedOffset = mySqlDebeziumStateUtil.savedOffset(MYSQL_PROPERTIES, CONFIGURED_CATALOG, - debeziumState.get("mysql_cdc_offset"), database.getSourceConfig()); - Assertions.assertTrue(parsedOffset.isPresent()); - Assertions.assertNotNull(parsedOffset.get().binlogFilename()); - Assertions.assertTrue(parsedOffset.get().binlogPosition() > 0); - Assertions.assertTrue(parsedOffset.get().gtidSet().isEmpty()); - container.stop(); - } - } - - @Test - public void formatTestWithGtid() { - final MySqlDebeziumStateUtil mySqlDebeziumStateUtil = new MySqlDebeziumStateUtil(); - final JsonNode debeziumState = mySqlDebeziumStateUtil.format(new MysqlDebeziumStateAttributes("binlog.000002", 633, - Optional.of("3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5")), "db_fgnfxvllud", "db_fgnfxvllud", Instant.parse("2023-06-06T08:36:10.341842Z")); - final Map stateAsMap = Jsons.object(debeziumState, Map.class); - Assertions.assertEquals(1, stateAsMap.size()); - Assertions.assertTrue(stateAsMap.containsKey("[\"db_fgnfxvllud\",{\"server\":\"db_fgnfxvllud\"}]")); - Assertions.assertEquals( - "{\"transaction_id\":null,\"ts_sec\":1686040570,\"file\":\"binlog.000002\",\"pos\":633,\"gtids\":\"3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5\"}", - stateAsMap.get("[\"db_fgnfxvllud\",{\"server\":\"db_fgnfxvllud\"}]")); - - final JsonNode config = Jsons.jsonNode(ImmutableMap.builder() - .put(JdbcUtils.HOST_KEY, "host") - .put(JdbcUtils.PORT_KEY, "5432") - .put(JdbcUtils.DATABASE_KEY, "db_fgnfxvllud") - .put(JdbcUtils.USERNAME_KEY, "username") - .put(JdbcUtils.PASSWORD_KEY, "password") - .put(JdbcUtils.SSL_KEY, false) - .build()); - - final Optional parsedOffset = mySqlDebeziumStateUtil.savedOffset(MYSQL_PROPERTIES, CONFIGURED_CATALOG, - debeziumState, config); - Assertions.assertTrue(parsedOffset.isPresent()); - final JsonNode stateGeneratedUsingParsedOffset = - mySqlDebeziumStateUtil.format(parsedOffset.get(), "db_fgnfxvllud", "db_fgnfxvllud", Instant.parse("2023-06-06T08:36:10.341842Z")); - Assertions.assertEquals(debeziumState, stateGeneratedUsingParsedOffset); - } - - @Test - public void formatTestWithoutGtid() { - final MySqlDebeziumStateUtil mySqlDebeziumStateUtil = new MySqlDebeziumStateUtil(); - final JsonNode debeziumState = mySqlDebeziumStateUtil.format(new MysqlDebeziumStateAttributes("binlog.000002", 633, - Optional.empty()), "db_fgnfxvllud", "db_fgnfxvllud", Instant.parse("2023-06-06T08:36:10.341842Z")); - final Map stateAsMap = Jsons.object(debeziumState, Map.class); - Assertions.assertEquals(1, stateAsMap.size()); - Assertions.assertTrue(stateAsMap.containsKey("[\"db_fgnfxvllud\",{\"server\":\"db_fgnfxvllud\"}]")); - Assertions.assertEquals("{\"transaction_id\":null,\"ts_sec\":1686040570,\"file\":\"binlog.000002\",\"pos\":633}", - stateAsMap.get("[\"db_fgnfxvllud\",{\"server\":\"db_fgnfxvllud\"}]")); - - final JsonNode config = Jsons.jsonNode(ImmutableMap.builder() - .put(JdbcUtils.HOST_KEY, "host") - .put(JdbcUtils.PORT_KEY, "5432") - .put(JdbcUtils.DATABASE_KEY, "db_fgnfxvllud") - .put(JdbcUtils.USERNAME_KEY, "username") - .put(JdbcUtils.PASSWORD_KEY, "password") - .put(JdbcUtils.SSL_KEY, false) - .build()); - - final Optional parsedOffset = mySqlDebeziumStateUtil.savedOffset(MYSQL_PROPERTIES, CONFIGURED_CATALOG, - debeziumState, config); - Assertions.assertTrue(parsedOffset.isPresent()); - final JsonNode stateGeneratedUsingParsedOffset = - mySqlDebeziumStateUtil.format(parsedOffset.get(), "db_fgnfxvllud", "db_fgnfxvllud", Instant.parse("2023-06-06T08:36:10.341842Z")); - Assertions.assertEquals(debeziumState, stateGeneratedUsingParsedOffset); - } - - private JdbcDatabase getJdbcDatabase(final MySQLContainer container) { - final JdbcDatabase database = new DefaultJdbcDatabase( - DataSourceFactory.create( - "root", - "test", - DatabaseDriver.MYSQL.getDriverClassName(), - String.format(DatabaseDriver.MYSQL.getUrlFormatString(), - container.getHost(), - container.getFirstMappedPort(), - DB_NAME))); - database.setSourceConfig(getSourceConfig(container)); - return database; - } - - private void initDB(final MySQLContainer container) throws SQLException { - final Database db = new Database(DSLContextFactory.create( - "root", - "test", - DatabaseDriver.MYSQL.getDriverClassName(), - String.format("jdbc:mysql://%s:%s", - container.getHost(), - container.getFirstMappedPort()), - SQLDialect.MYSQL)); - db.query(ctx -> ctx.execute(DB_CREATE_QUERY)); - db.query(ctx -> ctx.execute(TABLE_CREATE_QUERY)); - } - - private JsonNode getSourceConfig(final MySQLContainer container) { - final Map config = new HashMap<>(); - config.put(JdbcUtils.USERNAME_KEY, "root"); - config.put(JdbcUtils.PASSWORD_KEY, "test"); - config.put(JdbcUtils.HOST_KEY, container.getHost()); - config.put(JdbcUtils.PORT_KEY, container.getFirstMappedPort()); - config.put(JdbcUtils.DATABASE_KEY, DB_NAME); - return Jsons.jsonNode(config); - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/kotlin/MySqlSourceExceptionHandlerTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/MySqlSourceExceptionHandlerTest.kt deleted file mode 100644 index b5049d3c1b95..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/kotlin/MySqlSourceExceptionHandlerTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2024 Airbyte, Inc., all rights reserved. - */ - -import io.airbyte.cdk.integrations.util.FailureType -import io.airbyte.integrations.source.mysql.MySqlSourceExceptionHandler -import java.io.EOFException -import java.sql.SQLSyntaxErrorException -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test - -class MySqlSourceExceptionHandlerTest { - private var exceptionHandler: MySqlSourceExceptionHandler? = null - - @BeforeEach - fun setUp() { - exceptionHandler = MySqlSourceExceptionHandler() - } - - @Test - fun testTranslateMySQLSyntaxException() { - val exception = SQLSyntaxErrorException("Unknown column 'xmin' in 'field list'") - val externalMessage = exceptionHandler!!.getExternalMessage(exception) - Assertions.assertTrue(exceptionHandler!!.checkErrorType(exception, FailureType.CONFIG)) - Assertions.assertEquals( - "A column needed by MySQL source connector is missing in the database", - externalMessage - ) - } - - @Test - fun testTranslateEOFException() { - val exception = - EOFException( - "Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost." - ) - val externalMessage = exceptionHandler!!.getExternalMessage(exception) - Assertions.assertTrue(exceptionHandler!!.checkErrorType(exception, FailureType.TRANSIENT)) - Assertions.assertEquals("Can not read data from MySQL server", externalMessage) - } -} diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcDatatypeIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcDatatypeIntegrationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcDatatypeIntegrationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcDatatypeIntegrationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcIntegrationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcIntegrationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCdcIntegrationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlContainerFactory.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlContainerFactory.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlContainerFactory.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlContainerFactory.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCursorBasedIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCursorBasedIntegrationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCursorBasedIntegrationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlCursorBasedIntegrationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactoryTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactoryTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactoryTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlJdbcPartitionFactoryTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecificationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecificationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecificationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationSpecificationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceConfigurationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceDatatypeIntegrationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceSelectQueryGeneratorTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceSelectQueryGeneratorTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceSelectQueryGeneratorTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceSelectQueryGeneratorTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceTestConfigurationFactory.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceTestConfigurationFactory.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceTestConfigurationFactory.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSourceTestConfigurationFactory.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSpecIntegrationTest.kt b/airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSpecIntegrationTest.kt similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSpecIntegrationTest.kt rename to airbyte-integrations/connectors/source-mysql/src/test/kotlin/io/airbyte/integrations/source/mysql/MysqlSpecIntegrationTest.kt diff --git a/airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json b/airbyte-integrations/connectors/source-mysql/src/test/resources/expected-spec.json similarity index 100% rename from airbyte-integrations/connectors/source-mysql-v2/src/test/resources/expected-spec.json rename to airbyte-integrations/connectors/source-mysql/src/test/resources/expected-spec.json diff --git a/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_cloud_spec.json b/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_cloud_spec.json deleted file mode 100644 index 29c00d0c7b7a..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_cloud_spec.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/mysql", - "connectionSpecification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MySql Source Spec", - "type": "object", - "required": ["host", "port", "database", "username", "replication_method"], - "properties": { - "host": { - "description": "The host name of the database.", - "title": "Host", - "type": "string", - "order": 0 - }, - "port": { - "description": "The port to connect to.", - "title": "Port", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 3306, - "examples": ["3306"], - "order": 1 - }, - "database": { - "description": "The database name.", - "title": "Database", - "type": "string", - "order": 2 - }, - "username": { - "description": "The username which is used to access the database.", - "title": "Username", - "type": "string", - "order": 3 - }, - "password": { - "description": "The password associated with the username.", - "title": "Password", - "type": "string", - "airbyte_secret": true, - "order": 4, - "always_show": true - }, - "jdbc_url_params": { - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). For more information read about JDBC URL parameters.", - "title": "JDBC URL Parameters (Advanced)", - "type": "string", - "order": 5 - }, - "ssl_mode": { - "title": "SSL modes", - "description": "SSL connection modes. Read more in the docs.", - "type": "object", - "order": 7, - "oneOf": [ - { - "title": "preferred", - "description": "Automatically attempt SSL connection. If the MySQL server does not support SSL, continue with a regular connection.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "preferred", - "order": 0 - } - } - }, - { - "title": "required", - "description": "Always connect with SSL. If the MySQL server doesn’t support SSL, the connection will not be established. Certificate Authority (CA) and Hostname are not verified.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "required", - "order": 0 - } - } - }, - { - "title": "Verify CA", - "description": "Always connect with SSL. Verifies CA, but allows connection even if Hostname does not match.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_ca", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - }, - { - "title": "Verify Identity", - "description": "Always connect with SSL. Verify both CA and Hostname.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_identity", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - } - ], - "default": "required" - }, - "replication_method": { - "type": "object", - "title": "Update Method", - "description": "Configures how data is extracted from the database.", - "order": 8, - "default": "CDC", - "display_type": "radio", - "oneOf": [ - { - "title": "Read Changes using Binary Log (CDC)", - "description": "Recommended - Incrementally reads new inserts, updates, and deletes using the MySQL binary log. This must be enabled on your database.", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "CDC", - "order": 0 - }, - "initial_waiting_seconds": { - "type": "integer", - "title": "Initial Waiting Time in Seconds (Advanced)", - "description": "The amount of time the connector will wait when it launches to determine if there is new data to sync or not. Defaults to 300 seconds. Valid range: 120 seconds to 1200 seconds. Read about initial waiting time.", - "default": 300, - "min": 120, - "max": 1200, - "order": 1, - "always_show": true - }, - "server_time_zone": { - "type": "string", - "title": "Configured server timezone for the MySQL source (Advanced)", - "description": "Enter the configured MySQL server timezone. This should only be done if the configured timezone in your MySQL instance does not conform to IANNA standard.", - "order": 2, - "always_show": true - }, - "invalid_cdc_cursor_position_behavior": { - "type": "string", - "title": "Invalid CDC position behavior (Advanced)", - "description": "Determines whether Airbyte should fail or re-sync data in case of an stale/invalid cursor value into the WAL. If 'Fail sync' is chosen, a user will have to manually reset the connection before being able to continue syncing data. If 'Re-sync data' is chosen, Airbyte will automatically trigger a refresh but could lead to higher cloud costs and data loss.", - "enum": ["Fail sync", "Re-sync data"], - "default": "Fail sync", - "order": 3, - "always_show": true - }, - "initial_load_timeout_hours": { - "type": "integer", - "title": "Initial Load Timeout in Hours (Advanced)", - "description": "The amount of time an initial load is allowed to continue for before catching up on CDC logs.", - "default": 8, - "min": 4, - "max": 24, - "order": 4, - "always_show": true - } - } - }, - { - "title": "Scan Changes with User Defined Cursor", - "description": "Incrementally detects new inserts and updates using the cursor column chosen when configuring a connection (e.g. created_at, updated_at).", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "STANDARD", - "order": 0 - } - } - } - ] - } - } - } -} diff --git a/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_oss_spec.json b/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_oss_spec.json deleted file mode 100644 index 5a9304326cdd..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/test/resources/expected_oss_spec.json +++ /dev/null @@ -1,252 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/mysql", - "connectionSpecification": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MySql Source Spec", - "type": "object", - "required": ["host", "port", "database", "username", "replication_method"], - "properties": { - "host": { - "description": "The host name of the database.", - "title": "Host", - "type": "string", - "order": 0 - }, - "port": { - "description": "The port to connect to.", - "title": "Port", - "type": "integer", - "minimum": 0, - "maximum": 65536, - "default": 3306, - "examples": ["3306"], - "order": 1 - }, - "database": { - "description": "The database name.", - "title": "Database", - "type": "string", - "order": 2 - }, - "username": { - "description": "The username which is used to access the database.", - "title": "Username", - "type": "string", - "order": 3 - }, - "password": { - "description": "The password associated with the username.", - "title": "Password", - "type": "string", - "airbyte_secret": true, - "order": 4, - "always_show": true - }, - "jdbc_url_params": { - "description": "Additional properties to pass to the JDBC URL string when connecting to the database formatted as 'key=value' pairs separated by the symbol '&'. (example: key1=value1&key2=value2&key3=value3). For more information read about JDBC URL parameters.", - "title": "JDBC URL Parameters (Advanced)", - "type": "string", - "order": 5 - }, - "ssl": { - "title": "SSL Connection", - "description": "Encrypt data using SSL.", - "type": "boolean", - "default": true, - "order": 6 - }, - "ssl_mode": { - "title": "SSL modes", - "description": "SSL connection modes. Read more in the docs.", - "type": "object", - "order": 7, - "oneOf": [ - { - "title": "preferred", - "description": "Automatically attempt SSL connection. If the MySQL server does not support SSL, continue with a regular connection.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "preferred", - "order": 0 - } - } - }, - { - "title": "required", - "description": "Always connect with SSL. If the MySQL server doesn’t support SSL, the connection will not be established. Certificate Authority (CA) and Hostname are not verified.", - "required": ["mode"], - "properties": { - "mode": { - "type": "string", - "const": "required", - "order": 0 - } - } - }, - { - "title": "Verify CA", - "description": "Always connect with SSL. Verifies CA, but allows connection even if Hostname does not match.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_ca", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - }, - { - "title": "Verify Identity", - "description": "Always connect with SSL. Verify both CA and Hostname.", - "required": ["mode", "ca_certificate"], - "properties": { - "mode": { - "type": "string", - "const": "verify_identity", - "order": 0 - }, - "ca_certificate": { - "type": "string", - "title": "CA certificate", - "description": "CA certificate", - "airbyte_secret": true, - "multiline": true, - "order": 1 - }, - "client_certificate": { - "type": "string", - "title": "Client certificate", - "description": "Client certificate (this is not a required field, but if you want to use it, you will need to add the Client key as well)", - "airbyte_secret": true, - "multiline": true, - "order": 2, - "always_show": true - }, - "client_key": { - "type": "string", - "title": "Client key", - "description": "Client key (this is not a required field, but if you want to use it, you will need to add the Client certificate as well)", - "airbyte_secret": true, - "multiline": true, - "order": 3, - "always_show": true - }, - "client_key_password": { - "type": "string", - "title": "Client key password", - "description": "Password for keystorage. This field is optional. If you do not add it - the password will be generated automatically.", - "airbyte_secret": true, - "order": 4 - } - } - } - ] - }, - "replication_method": { - "type": "object", - "title": "Update Method", - "description": "Configures how data is extracted from the database.", - "order": 8, - "default": "CDC", - "display_type": "radio", - "oneOf": [ - { - "title": "Read Changes using Binary Log (CDC)", - "description": "Recommended - Incrementally reads new inserts, updates, and deletes using the MySQL binary log. This must be enabled on your database.", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "CDC", - "order": 0 - }, - "initial_waiting_seconds": { - "type": "integer", - "title": "Initial Waiting Time in Seconds (Advanced)", - "description": "The amount of time the connector will wait when it launches to determine if there is new data to sync or not. Defaults to 300 seconds. Valid range: 120 seconds to 1200 seconds. Read about initial waiting time.", - "default": 300, - "min": 120, - "max": 1200, - "order": 1, - "always_show": true - }, - "server_time_zone": { - "type": "string", - "title": "Configured server timezone for the MySQL source (Advanced)", - "description": "Enter the configured MySQL server timezone. This should only be done if the configured timezone in your MySQL instance does not conform to IANNA standard.", - "order": 2, - "always_show": true - }, - "invalid_cdc_cursor_position_behavior": { - "type": "string", - "title": "Invalid CDC position behavior (Advanced)", - "description": "Determines whether Airbyte should fail or re-sync data in case of an stale/invalid cursor value into the WAL. If 'Fail sync' is chosen, a user will have to manually reset the connection before being able to continue syncing data. If 'Re-sync data' is chosen, Airbyte will automatically trigger a refresh but could lead to higher cloud costs and data loss.", - "enum": ["Fail sync", "Re-sync data"], - "default": "Fail sync", - "order": 3, - "always_show": true - }, - "initial_load_timeout_hours": { - "type": "integer", - "title": "Initial Load Timeout in Hours (Advanced)", - "description": "The amount of time an initial load is allowed to continue for before catching up on CDC logs.", - "default": 8, - "min": 4, - "max": 24, - "order": 4, - "always_show": true - } - } - }, - { - "title": "Scan Changes with User Defined Cursor", - "description": "Incrementally detects new inserts and updates using the cursor column chosen when configuring a connection (e.g. created_at, updated_at).", - "required": ["method"], - "properties": { - "method": { - "type": "string", - "const": "STANDARD", - "order": 0 - } - } - } - ] - } - } - } -} diff --git a/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLContainerFactory.java b/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLContainerFactory.java deleted file mode 100644 index 2e9fba65fbb7..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLContainerFactory.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import io.airbyte.cdk.testutils.ContainerFactory; -import java.io.IOException; -import java.io.UncheckedIOException; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; - -public class MySQLContainerFactory extends ContainerFactory> { - - @Override - protected MySQLContainer createNewContainer(DockerImageName imageName) { - return new MySQLContainer<>(imageName.asCompatibleSubstituteFor("mysql")); - } - - /** - * Create a new network and bind it to the container. - */ - public void withNetwork(MySQLContainer container) { - container.withNetwork(Network.newNetwork()); - } - - private static final String INVALID_TIMEZONE_CEST = "CEST"; - - public void withInvalidTimezoneCEST(MySQLContainer container) { - container.withEnv("TZ", INVALID_TIMEZONE_CEST); - } - - public void withMoscowTimezone(MySQLContainer container) { - container.withEnv("TZ", "Europe/Moscow"); - } - - public void withCustomName(MySQLContainer container) {} // do nothing - - public void withRootAndServerCertificates(MySQLContainer container) { - execInContainer(container, - "sed -i '31 a ssl' /etc/my.cnf", - "sed -i '32 a ssl-ca=/var/lib/mysql/ca.pem' /etc/my.cnf", - "sed -i '33 a ssl-cert=/var/lib/mysql/server-cert.pem' /etc/my.cnf", - "sed -i '34 a ssl-key=/var/lib/mysql/server-key.pem' /etc/my.cnf", - "sed -i '35 a require_secure_transport=ON' /etc/my.cnf"); - } - - public void withClientCertificate(MySQLContainer container) { - execInContainer(container, - "sed -i '39 a [client]' /etc/mysql/my.cnf", - "sed -i '40 a ssl-ca=/var/lib/mysql/ca.pem' /etc/my.cnf", - "sed -i '41 a ssl-cert=/var/lib/mysql/client-cert.pem' /etc/my.cnf", - "sed -i '42 a ssl-key=/var/lib/mysql/client-key.pem' /etc/my.cnf"); - } - - static private void execInContainer(MySQLContainer container, String... commands) { - container.start(); - try { - for (String command : commands) { - container.execInContainer("sh", "-c", command); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLTestDatabase.java b/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLTestDatabase.java deleted file mode 100644 index 219d5e90f479..000000000000 --- a/airbyte-integrations/connectors/source-mysql/src/testFixtures/java/io/airbyte/integrations/source/mysql/MySQLTestDatabase.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright (c) 2023 Airbyte, Inc., all rights reserved. - */ - -package io.airbyte.integrations.source.mysql; - -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.INVALID_CDC_CURSOR_POSITION_PROPERTY; -import static io.airbyte.integrations.source.mysql.MySqlSpecConstants.RESYNC_DATA_OPTION; - -import com.google.common.collect.ImmutableMap; -import io.airbyte.cdk.db.factory.DatabaseDriver; -import io.airbyte.cdk.testutils.TestDatabase; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.jooq.SQLDialect; -import org.testcontainers.containers.MySQLContainer; - -public class MySQLTestDatabase extends - TestDatabase, MySQLTestDatabase, MySQLTestDatabase.MySQLConfigBuilder> { - - public enum BaseImage { - - MYSQL_8("mysql:8.0"), - ; - - public final String reference; - - BaseImage(String reference) { - this.reference = reference; - } - - } - - public enum ContainerModifier { - - MOSCOW_TIMEZONE("withMoscowTimezone"), - INVALID_TIMEZONE_CEST("withInvalidTimezoneCEST"), - ROOT_AND_SERVER_CERTIFICATES("withRootAndServerCertificates"), - CLIENT_CERTITICATE("withClientCertificate"), - NETWORK("withNetwork"), - - CUSTOM_NAME("withCustomName"); - - public final String methodName; - - ContainerModifier(String methodName) { - this.methodName = methodName; - } - - } - - static public MySQLTestDatabase in(BaseImage baseImage, ContainerModifier... methods) { - String[] methodNames = Stream.of(methods).map(im -> im.methodName).toList().toArray(new String[0]); - final var container = new MySQLContainerFactory().shared(baseImage.reference, methodNames); - return new MySQLTestDatabase(container).initialized(); - } - - public MySQLTestDatabase(MySQLContainer container) { - super(container); - } - - public MySQLTestDatabase withCdcPermissions() { - return this - .with("REVOKE ALL PRIVILEGES, GRANT OPTION FROM '%s';", getUserName()) - .with("GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '%s';", getUserName()); - } - - public MySQLTestDatabase withoutStrictMode() { - // This disables strict mode in the DB and allows to insert specific values. - // For example, it's possible to insert date with zero values "2021-00-00" - return with("SET @@sql_mode=''"); - } - - static private final int MAX_CONNECTIONS = 1000; - - @Override - protected Stream> inContainerBootstrapCmd() { - // Besides setting up user and privileges, we also need to create a soft link otherwise - // airbyte-ci on github runner would not be able to connect to DB, because the sock file does not - // exist. - return Stream.of(Stream.of( - "sh", "-c", "ln -s -f /var/lib/mysql/mysql.sock /var/run/mysqld/mysqld.sock"), - mysqlCmd(Stream.of( - String.format("SET GLOBAL max_connections=%d", MAX_CONNECTIONS), - String.format("CREATE DATABASE \\`%s\\`", getDatabaseName()), - String.format("CREATE USER '%s' IDENTIFIED BY '%s'", getUserName(), getPassword()), - // Grant privileges also to the container's user, which is not root. - String.format("GRANT ALL PRIVILEGES ON *.* TO '%s', '%s' WITH GRANT OPTION", getUserName(), - getContainer().getUsername())))); - - } - - @Override - protected Stream inContainerUndoBootstrapCmd() { - return mysqlCmd(Stream.of( - String.format("DROP USER '%s'", getUserName()), - String.format("DROP DATABASE \\`%s\\`", getDatabaseName()))); - } - - @Override - public DatabaseDriver getDatabaseDriver() { - return DatabaseDriver.MYSQL; - } - - @Override - public SQLDialect getSqlDialect() { - return SQLDialect.MYSQL; - } - - @Override - public MySQLConfigBuilder configBuilder() { - return new MySQLConfigBuilder(this); - } - - public Stream mysqlCmd(Stream sql) { - return Stream.of("bash", "-c", String.format( - "set -o errexit -o pipefail; echo \"%s\" | mysql -v -v -v --user=root --password=test", - sql.collect(Collectors.joining("; ")))); - } - - static public class MySQLConfigBuilder extends ConfigBuilder { - - protected MySQLConfigBuilder(MySQLTestDatabase testDatabase) { - super(testDatabase); - } - - public MySQLConfigBuilder withStandardReplication() { - return with("replication_method", ImmutableMap.builder().put("method", "STANDARD").build()); - } - - public MySQLConfigBuilder withCdcReplication() { - return withCdcReplication(RESYNC_DATA_OPTION); - } - - public MySQLConfigBuilder withCdcReplication(String cdcCursorFailBehaviour) { - return this - .with("is_test", true) - .with("replication_method", ImmutableMap.builder() - .put("method", "CDC") - .put("initial_waiting_seconds", 5) - .put("server_time_zone", "America/Los_Angeles") - .put(INVALID_CDC_CURSOR_POSITION_PROPERTY, cdcCursorFailBehaviour) - .build()); - } - - } - - private String cachedCaCertificate; - private Certificates cachedCertificates; - - public synchronized String getCaCertificate() { - if (cachedCaCertificate == null) { - cachedCaCertificate = catFileInContainer("/var/lib/mysql/ca.pem"); - } - return cachedCaCertificate; - } - - public synchronized Certificates getCertificates() { - if (cachedCertificates == null) { - cachedCertificates = new Certificates( - catFileInContainer("/var/lib/mysql/ca.pem"), - catFileInContainer("/var/lib/mysql/client-cert.pem"), - catFileInContainer("/var/lib/mysql/client-key.pem")); - } - return cachedCertificates; - } - - public record Certificates(String caCertificate, String clientCertificate, String clientKey) {} - - private String catFileInContainer(String filePath) { - try { - return getContainer().execInContainer("sh", "-c", "cat " + filePath).getStdout().trim(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - -} diff --git a/docs/integrations/sources/mysql.md b/docs/integrations/sources/mysql.md index 6f7af411774a..7989acafb524 100644 --- a/docs/integrations/sources/mysql.md +++ b/docs/integrations/sources/mysql.md @@ -226,9 +226,10 @@ Any database or table encoding combination of charset and collation is supported | Version | Date | Pull Request | Subject | |:--------|:-----------|:-----------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| -| 3.7.3 | 2024-09-17 | [45639](https://github.com/airbytehq/airbyte/pull/45639) | Adopt latest CDK to use the latest apache sshd mina to handle tcpkeepalive requests. | -| 3.7.2 | 2024-09-05 | [45181](https://github.com/airbytehq/airbyte/pull/45181) | Fix incorrect categorizing resumable/nonresumable full refresh streams. | -| 3.7.1 | 2024-08-27 | [44841](https://github.com/airbytehq/airbyte/pull/44841) | Adopt latest CDK. | +| 3.9.0-rc.1 | 2024-11-05 | [48369](https://github.com/airbytehq/airbyte/pull/48369) | Progressive rollout test. | +| 3.7.3 | 2024-09-17 | [45639](https://github.com/airbytehq/airbyte/pull/45639) | Adopt latest CDK to use the latest apache sshd mina to handle tcpkeepalive requests. | +| 3.7.2 | 2024-09-05 | [45181](https://github.com/airbytehq/airbyte/pull/45181) | Fix incorrect categorizing resumable/nonresumable full refresh streams. | +| 3.7.1 | 2024-08-27 | [44841](https://github.com/airbytehq/airbyte/pull/44841) | Adopt latest CDK. | | 3.7.0 | 2024-08-13 | [44013](https://github.com/airbytehq/airbyte/pull/44013) | Upgrading to Debezium 2.7.1.Final | | 3.6.9 | 2024-08-08 | [43410](https://github.com/airbytehq/airbyte/pull/43410) | Adopt latest CDK. | | 3.6.8 | 2024-07-30 | [42869](https://github.com/airbytehq/airbyte/pull/42869) | Adopt latest CDK. |