diff --git a/airbyte-config-oss/init-oss/src/main/resources/seed/source_definitions.yaml b/airbyte-config-oss/init-oss/src/main/resources/seed/source_definitions.yaml index 405834306dcd..2c156f7e5e17 100644 --- a/airbyte-config-oss/init-oss/src/main/resources/seed/source_definitions.yaml +++ b/airbyte-config-oss/init-oss/src/main/resources/seed/source_definitions.yaml @@ -2061,6 +2061,17 @@ allowedHosts: hosts: - api.tempo.io +- name: Teradata + sourceDefinitionId: aa8ba6fd-4875-d94e-fc8d-4e1e09aa2503 + dockerRepository: airbyte/source-teradata + dockerImageTag: 0.1.0 + documentationUrl: https://docs.airbyte.com/integrations/sources/teradata + icon: teradata.svg + sourceType: database + releaseStage: alpha + allowedHosts: + hosts: + - "${host}" - name: TiDB sourceDefinitionId: 0dad1a35-ccf8-4d03-b73e-6788c00b13ae dockerRepository: airbyte/source-tidb diff --git a/airbyte-config-oss/init-oss/src/main/resources/seed/source_specs.yaml b/airbyte-config-oss/init-oss/src/main/resources/seed/source_specs.yaml index 2fd731c9f59d..294b705c9426 100644 --- a/airbyte-config-oss/init-oss/src/main/resources/seed/source_specs.yaml +++ b/airbyte-config-oss/init-oss/src/main/resources/seed/source_specs.yaml @@ -15429,6 +15429,180 @@ supportsNormalization: false supportsDBT: false supported_destination_sync_modes: [] +- dockerImage: "airbyte/source-teradata:0.1.0" + spec: + documentationUrl: "https://docs.airbyte.com/integrations/sources/teradata" + connectionSpecification: + $schema: "http://json-schema.org/draft-07/schema#" + title: "Teradata Source Spec" + type: "object" + required: + - "host" + - "database" + - "username" + properties: + host: + title: "Host" + description: "Hostname of the database." + type: "string" + order: 0 + port: + title: "Port" + description: "Port of the database." + type: "integer" + minimum: 0 + maximum: 65536 + default: 3306 + examples: + - "3306" + order: 1 + database: + title: "Database" + description: "Name of the database." + type: "string" + order: 2 + username: + title: "Username" + description: "Username to use to access the database." + type: "string" + order: 3 + password: + title: "Password" + description: "Password associated with the username." + type: "string" + airbyte_secret: true + order: 4 + jdbc_url_params: + title: "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)" + type: "string" + order: 5 + replication_method: + title: "Replication method" + description: "Replication method to use for extracting data from the database.\ + \ STANDARD replication requires no setup on the DB side but will not be\ + \ able to represent deletions incrementally. CDC uses the Binlog to detect\ + \ inserts, updates, and deletes. This needs to be configured on the source\ + \ database itself." + type: "string" + order: 6 + default: "STANDARD" + enum: + - "STANDARD" + - "CDC" + ssl: + title: "SSL Connection" + description: "Encrypt data using SSL. When activating SSL, please select\ + \ one of the connection modes." + type: "boolean" + default: false + order: 7 + ssl_mode: + title: "SSL Modes" + description: "SSL connection modes. \n disable - Chose this mode\ + \ to disable encryption of communication between Airbyte and destination\ + \ database\n allow - Chose this mode to enable encryption only\ + \ when required by the destination database\n prefer - Chose this\ + \ mode to allow unencrypted connection only if the destination database\ + \ does not support encryption\n require - Chose this mode to always\ + \ require encryption. If the destination database server does not support\ + \ encryption, connection will fail\n verify-ca - Chose this mode\ + \ to always require encryption and to verify that the destination database\ + \ server has a valid SSL certificate\n verify-full - This is the\ + \ most secure mode. Chose this mode to always require encryption and to\ + \ verify the identity of the destination database server\n See more information\ + \ - in the docs." + type: "object" + order: 8 + oneOf: + - title: "disable" + additionalProperties: true + description: "Disable SSL." + required: + - "mode" + properties: + mode: + type: "string" + const: "disable" + order: 0 + - title: "allow" + additionalProperties: true + description: "Allow SSL mode." + required: + - "mode" + properties: + mode: + type: "string" + const: "allow" + order: 0 + - title: "prefer" + additionalProperties: true + description: "Prefer SSL mode." + required: + - "mode" + properties: + mode: + type: "string" + const: "prefer" + order: 0 + - title: "require" + additionalProperties: true + description: "Require SSL mode." + required: + - "mode" + properties: + mode: + type: "string" + const: "require" + order: 0 + - title: "verify-ca" + additionalProperties: true + description: "Verify-ca SSL mode." + required: + - "mode" + - "ssl_ca_certificate" + properties: + mode: + type: "string" + const: "verify-ca" + order: 0 + ssl_ca_certificate: + type: "string" + title: "CA certificate" + description: "Specifies the file name of a PEM file that contains\ + \ Certificate Authority (CA) certificates for use with SSLMODE=verify-ca.\n\ + \ See more information - in the docs." + airbyte_secret: true + multiline: true + order: 1 + - title: "verify-full" + additionalProperties: true + description: "Verify-full SSL mode." + required: + - "mode" + - "ssl_ca_certificate" + properties: + mode: + type: "string" + const: "verify-full" + order: 0 + ssl_ca_certificate: + type: "string" + title: "CA certificate" + description: "Specifies the file name of a PEM file that contains\ + \ Certificate Authority (CA) certificates for use with SSLMODE=verify-full.\n\ + \ See more information - in the docs." + airbyte_secret: true + multiline: true + order: 1 + supportsNormalization: false + supportsDBT: false + supported_destination_sync_modes: [] - dockerImage: "airbyte/source-tidb:0.2.4" spec: documentationUrl: "https://docs.airbyte.com/integrations/sources/tidb" diff --git a/airbyte-integrations/builds.md b/airbyte-integrations/builds.md index 1a1d75ef7d90..647584fd328d 100644 --- a/airbyte-integrations/builds.md +++ b/airbyte-integrations/builds.md @@ -3,8 +3,8 @@ # Sources -| name | status | -|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | status | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 3PL Central | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-tplcentral%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-tplcentral) | | Adjust | [![source-adjust](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-adjust%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-adjust) | | Airtable | [![source-airtable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-airtable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-airtable) | @@ -12,7 +12,7 @@ | Amazon Seller Partner | [![source-amazon-seller-partner](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-amazon-seller-partner%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-amazon-seller-partner) | | Amplitude | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-amplitude%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-amplitude) | | Apify Dataset | [![source-amplitude](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-apify-dataset%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-apify-dataset) | -| AppsFlyer | [![source-appsflyer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-appsflyer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-appsflyer-singer) | +| AppsFlyer | [![source-appsflyer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-appsflyer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-appsflyer-singer) | | App Store | [![source-appstore-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-appstore-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-appstore-singer) | | Asana | [![source-asana](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-asana%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-asana) | | Ashby | [![source-ashby](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-ashby%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-ashby) | @@ -61,7 +61,7 @@ | IBM Db2 | [![source-db2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-db2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-db2) | | Insightly | [![source-insightly](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-insightly%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-insightly) | | Instagram | [![source-instagram](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-instagram%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-instagram) | -| Intercom | [![source-intercom](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-intercom%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-intercom) | +| Intercom | [![source-intercom](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-intercom%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-intercom) | | Iterable | [![source-iterable](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-iterable%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-iterable) | | Jira | [![source-jira](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-jira%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-jira) | | LinkedIn Ads | [![source-linkedin-ads](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-linkedin-ads%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-linkedin-ads) | @@ -103,7 +103,7 @@ | Posthog | [![source-posthog](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-posthog%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-posthog) | | PrestaShop | [![source-prestashop](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-prestashop%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-prestashop) | | Primetric | [![source-primetric](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-primetric%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-primetric) | -| PyPI | [![source-public-apis](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pypi%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-pypi) | +| PyPI | [![source-public-apis](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fsource-pypi%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-pypi) | | Public APIs | [![source-public-apis](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-public-apis%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-public-apis) | | CockroachDb | [![source-cockroachdb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-cockroachdb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-cockroachdb) | | Confluence | [![source-confluence](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-confluence%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-confluence) | @@ -139,9 +139,9 @@ | US Census | [![source-us-census](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-us-census%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-us-census) | | Vitally | [![source-vitally](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-vitally%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-vitally) | | Visma e-conomics | [![source-visma-economic](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-visma-economic%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-us-census) | -| Waiteraid | [![source-waiteraid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-waiteraid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-waiteraid) | +| Waiteraid | [![source-waiteraid](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-waiteraid%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-waiteraid) | | Whisky Hunter | [![source-whisky-hunter](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-whisky-hunter%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-whisky-hunter) | -| Workramp | [![source-workramp](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-workramp%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-workramp) | +| Workramp | [![source-workramp](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-workramp%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-workramp) | | Wrike | [![source-wrike](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-wrike%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-wrike) | | YouTube Analytics | [![source-youtube-analytics](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-youtube-analytics%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-youtube-analytics) | | Weatherstack | [![source-weatherstack](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-weatherstack%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-weatherstack) | @@ -152,11 +152,12 @@ | Zendesk Talk | [![source-zendesk-talk](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-zendesk-talk%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-zendesk-talk) | | Zoom | [![source-zoom-singer](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-zoom-singer%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-zoom-singer) | | Zuora | [![source-zuora](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fsource-zuora%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/source-zuora) | +| Teradata | (Not Setup) | # Destinations -| name | status | -|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | status | +|:---------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Azure Blob Storage | [![destination-azure-blob-storage](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-azure-blob-storage%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-azure-blob-storage) | | BigQuery | [![destination-bigquery](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-bigquery%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-bigquery) | | BigQuery Denormalized | [![destination-bigquery-denormalized](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-bigquery-denormalized%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-bigquery-denormalized) | @@ -164,13 +165,13 @@ | Cassandra | [![destination-cassandra](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-cassandra%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-cassandra) | | Databricks | [![destination-databricks](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-databricks%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-databricks) | | Dev Null | [![destination-dev-null](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-dev-null%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-dev-null) | -| Elasticsearch | [![destination-elasticsearch](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-elasticsearch%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-elasticsearch) | +| Elasticsearch | [![destination-elasticsearch](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-elasticsearch%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-elasticsearch) | | End-to-End Testing | [![destination-e2e-test](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-e2e-test%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-e2e-test) | | Exasol | [![destination-exasol](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-exasol%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-exasol) | | Google Cloud Storage (GCS) | [![destination-gcs](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-gcs%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-gcs) | | Google Firestore | [![destination-firestore](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-firestore%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-firestore) | | Google PubSub | [![destination-pubsub](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-pubsub%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-pubsub) | -| Google Sheets | [![destination-google-sheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-google-sheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-google-sheets) | | +| Google Sheets | [![destination-google-sheets](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-google-sheets%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-google-sheets) | | Apache Iceberg | [![destination-iceberg](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-iceberg%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-iceberg) | | Kafka | [![destination-kafka](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-kafka%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-kafka) | | Keen (Chargify) | [![destination-keen](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-keen%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-keen) | @@ -184,7 +185,7 @@ | Oracle | [![destination-oracle](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-oracle%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-oracle) | | Postgres | [![destination-postgres](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-postgres%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-postgres) | | Pulsar | [![destination-pulsar](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-pulsar%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-pulsar) | -| R2 | [![destination-r2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-r2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-r2) | +| R2 | [![destination-r2](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-r2%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-r2) | | Redshift | [![destination-redshift](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-redshift%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-redshift) | | Rockset | [![destination-rockset](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-rockset%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-rockset) | | S3 | [![destination-s3](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-s3%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-s3) | @@ -192,4 +193,4 @@ | SFTP-JSON | [![destination-sftp-json](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-sftp-json%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-sftp-json) | | Snowflake | [![destination-snowflake](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-snowflake%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-snowflake) | | Teradata | [![destination-teradata](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-teradata%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-teradata) | -| TiDB | [![destination-tidb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-tidb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-tidb) | \ No newline at end of file +| TiDB | [![destination-tidb](https://img.shields.io/endpoint?url=https%3A%2F%2Fdnsgjos7lj2fu.cloudfront.net%2Ftests%2Fsummary%2Fconnectors%2Fdestination-tidb%2Fbadge.json)](https://dnsgjos7lj2fu.cloudfront.net/tests/summary/connectors/destination-tidb) | diff --git a/airbyte-integrations/connectors/source-jdbc/src/testFixtures/java/io/airbyte/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-jdbc/src/testFixtures/java/io/airbyte/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java index f922d27b1db2..bba9eca3db41 100644 --- a/airbyte-integrations/connectors/source-jdbc/src/testFixtures/java/io/airbyte/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java +++ b/airbyte-integrations/connectors/source-jdbc/src/testFixtures/java/io/airbyte/integrations/source/jdbc/test/JdbcSourceAcceptanceTest.java @@ -389,7 +389,8 @@ protected AirbyteCatalog filterOutOtherSchemas(final AirbyteCatalog catalog) { @Test void testDiscoverWithMultipleSchemas() throws Exception { // clickhouse and mysql do not have a concept of schemas, so this test does not make sense for them. - if (getDriverClass().toLowerCase().contains("mysql") || getDriverClass().toLowerCase().contains("clickhouse")) { + String driverClass = getDriverClass().toLowerCase(); + if (driverClass.contains("mysql") || driverClass.contains("clickhouse") || driverClass.contains("teradata")) { return; } @@ -836,10 +837,11 @@ protected void incrementalCursorCheck( // See https://github.com/airbytehq/airbyte/issues/14732 for rationale and details. @Test - void testIncrementalWithConcurrentInsertion() throws Exception { + public void testIncrementalWithConcurrentInsertion() throws Exception { + final String driverName = getDriverClass().toLowerCase(); final String namespace = getDefaultNamespace(); final String fullyQualifiedTableName = getFullyQualifiedTableName(TABLE_NAME_AND_TIMESTAMP); - final String columnDefinition = String.format("name VARCHAR(200) NOT NULL, timestamp %s NOT NULL", COL_TIMESTAMP_TYPE); + final String columnDefinition = String.format("name VARCHAR(200) NOT NULL, %s %s NOT NULL", COL_TIMESTAMP, COL_TIMESTAMP_TYPE); // 1st sync database.execute(ctx -> { @@ -877,7 +879,12 @@ void testIncrementalWithConcurrentInsertion() throws Exception { .filter(r -> r.getType() == Type.RECORD) .map(r -> r.getRecord().getData().get(COL_NAME).asText()) .toList(); - assertEquals(List.of("a", "b"), firstSyncNames); + // teradata doesn't make insertion order guarantee when equal ordering value + if (driverName.contains("teradata")) { + assertThat(List.of("a", "b"), Matchers.containsInAnyOrder(firstSyncNames.toArray())); + } else { + assertEquals(List.of("a", "b"), firstSyncNames); + } // 2nd sync database.execute(ctx -> { @@ -927,7 +934,14 @@ void testIncrementalWithConcurrentInsertion() throws Exception { .filter(r -> r.getType() == Type.RECORD) .map(r -> r.getRecord().getData().get(COL_NAME).asText()) .toList(); - assertEquals(List.of("c", "d", "e", "f"), thirdSyncExpectedNames); + + // teradata doesn't make insertion order guarantee when equal ordering value + if (driverName.contains("teradata")) { + assertThat(List.of("c", "d", "e", "f"), Matchers.containsInAnyOrder(thirdSyncExpectedNames.toArray())); + } else { + assertEquals(List.of("c", "d", "e", "f"), thirdSyncExpectedNames); + } + } private JsonNode getStateData(final AirbyteMessage airbyteMessage, final String streamName) { @@ -1158,7 +1172,8 @@ private String getDefaultSchemaName() { protected String getDefaultNamespace() { // mysql does not support schemas. it namespaces using database names instead. - if (getDriverClass().toLowerCase().contains("mysql") || getDriverClass().toLowerCase().contains("clickhouse")) { + if (getDriverClass().toLowerCase().contains("mysql") || getDriverClass().toLowerCase().contains("clickhouse") || + getDriverClass().toLowerCase().contains("teradata")) { return config.get(JdbcUtils.DATABASE_KEY).asText(); } else { return SCHEMA_NAME; diff --git a/airbyte-integrations/connectors/source-teradata/Dockerfile b/airbyte-integrations/connectors/source-teradata/Dockerfile new file mode 100644 index 000000000000..eec3a3ac0810 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/Dockerfile @@ -0,0 +1,21 @@ +FROM airbyte/integration-base-java:dev AS build + +WORKDIR /airbyte + +ENV APPLICATION source-teradata + +COPY build/distributions/${APPLICATION}*.tar ${APPLICATION}.tar + +RUN tar xf ${APPLICATION}.tar --strip-components=1 && rm -rf ${APPLICATION}.tar + +FROM airbyte/integration-base-java:dev + +WORKDIR /airbyte + +ENV APPLICATION source-teradata + +COPY --from=build /airbyte /airbyte + +# Airbyte's build system uses these labels to know what to name and tag the docker images produced by this Dockerfile. +LABEL io.airbyte.version=0.1.0 +LABEL io.airbyte.name=airbyte/source-teradata diff --git a/airbyte-integrations/connectors/source-teradata/README.md b/airbyte-integrations/connectors/source-teradata/README.md new file mode 100644 index 000000000000..ad9a67489aa1 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/README.md @@ -0,0 +1,69 @@ +# Source Teradata + +This is the repository for the Teradata source connector in Java. +For information about how to use this connector within Airbyte, see [the User Documentation](https://docs.airbyte.com/integrations/sources/teradata). + +## Local development + +#### Building via Gradle +From the Airbyte repository root, run: +``` +./gradlew :airbyte-integrations:connectors:source-teradata:build +``` + +#### Create credentials +**If you are a community contributor**, generate the necessary credentials and place them in `secrets/config.json` conforming to the spec file in `src/main/resources/spec.json`. +Note that the `secrets` directory is git-ignored by default, so there is no danger of accidentally checking in sensitive information. + +**If you are an Airbyte core member**, follow the [instructions](https://docs.airbyte.com/connector-development#using-credentials-in-ci) to set up the credentials. + +### Locally running the connector docker image + +#### Build +Build the connector image via Gradle: +``` +./gradlew :airbyte-integrations:connectors:source-teradata:airbyteDocker +``` +When building via Gradle, the docker image name and tag, respectively, are the values of the `io.airbyte.name` and `io.airbyte.version` `LABEL`s in +the Dockerfile. + +#### Run +Then run any of the connector commands as follows: +``` +docker run --rm airbyte/source-teradata:dev spec +docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-teradata:dev check --config /secrets/config.json +docker run --rm -v $(pwd)/secrets:/secrets airbyte/source-teradata:dev discover --config /secrets/config.json +docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/source-teradata:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json +``` + +## Testing +We use `JUnit` for Java tests. + +### Unit and Integration Tests +Place unit tests under `src/test/...` +Place integration tests in `src/test-integration/...` + +#### Acceptance Tests +Airbyte has a standard test suite that all source connectors must pass. Implement the `TODO`s in +`src/test-integration/java/io/airbyte/integrations/sources/TeradataSourceAcceptanceTest.java`. + +### Using gradle to run tests +All commands should be run from airbyte project root. +To run unit tests: +``` +./gradlew :airbyte-integrations:connectors:source-teradata:unitTest +``` +To run acceptance and custom integration tests: +``` +./gradlew :airbyte-integrations:connectors:source-teradata:integrationTest +``` + +## Dependency Management + +### Publishing a new version of the connector +You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what? +1. Make sure your changes are passing unit and integration tests. +1. Bump the connector version in `Dockerfile` -- just increment the value of the `LABEL io.airbyte.version` appropriately (we use [SemVer](https://semver.org/)). +1. Create a Pull Request. +1. Pat yourself on the back for being an awesome contributor. +1. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master. diff --git a/airbyte-integrations/connectors/source-teradata/acceptance-test-config.yml b/airbyte-integrations/connectors/source-teradata/acceptance-test-config.yml new file mode 100644 index 000000000000..49f39cead0ef --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/acceptance-test-config.yml @@ -0,0 +1,8 @@ +# 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-teradata:dev +acceptance_tests: + spec: + tests: + - spec_path: "src/test-integration/resources/expected_spec.json" + config_path: "secrets/config.json" diff --git a/airbyte-integrations/connectors/source-teradata/acceptance-test-docker.sh b/airbyte-integrations/connectors/source-teradata/acceptance-test-docker.sh new file mode 100644 index 000000000000..394835b93b09 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/acceptance-test-docker.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env sh + +# Build latest connector image +docker build . -t $(cat acceptance-test-config.yml | grep "connector_image" | head -n 1 | cut -d: -f2-) + +# Pull latest acctest image +docker pull airbyte/connector-acceptance-test:latest + +# Run +docker run --rm -it \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /tmp:/tmp \ + -v $(pwd):/test_input \ + airbyte/connector-acceptance-test \ + --acceptance-test-config /test_input diff --git a/airbyte-integrations/connectors/source-teradata/build.gradle b/airbyte-integrations/connectors/source-teradata/build.gradle new file mode 100644 index 000000000000..47b083feee8a --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'application' + id 'airbyte-docker' + id 'airbyte-integration-test-java' + id 'airbyte-connector-acceptance-test' +} + +application { + mainClass = 'io.airbyte.integrations.source.teradata.TeradataSource' +} + +dependencies { + implementation project(':airbyte-db:db-lib') + implementation project(':airbyte-integrations:bases:base-java') + implementation libs.airbyte.protocol + implementation project(':airbyte-integrations:connectors:source-jdbc') + implementation project(':airbyte-integrations:connectors:source-relational-db') + + implementation 'com.teradata.jdbc:terajdbc:20.00.00.06' + + testImplementation testFixtures(project(':airbyte-integrations:connectors:source-jdbc')) + + testImplementation 'org.apache.commons:commons-lang3:3.11' + + integrationTestJavaImplementation project(':airbyte-integrations:connectors:source-teradata') + integrationTestJavaImplementation project(':airbyte-integrations:bases:standard-source-test') + + implementation files(project(':airbyte-integrations:bases:base-java').airbyteDocker.outputs) + integrationTestJavaImplementation files(project(':airbyte-integrations:bases:base-java').airbyteDocker.outputs) +} diff --git a/airbyte-integrations/connectors/source-teradata/integration_tests/acceptance.py b/airbyte-integrations/connectors/source-teradata/integration_tests/acceptance.py new file mode 100644 index 000000000000..9e6409236281 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/integration_tests/acceptance.py @@ -0,0 +1,16 @@ +# +# 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-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSource.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSource.java new file mode 100644 index 000000000000..a19a01509df5 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSource.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.source.teradata; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.collect.ImmutableMap; +import io.airbyte.commons.json.Jsons; +import io.airbyte.commons.map.MoreMaps; +import io.airbyte.db.factory.DataSourceFactory; +import io.airbyte.db.jdbc.JdbcDatabase; +import io.airbyte.db.jdbc.JdbcUtils; +import io.airbyte.db.jdbc.StreamingJdbcDatabase; +import io.airbyte.db.jdbc.streaming.AdaptiveStreamingQueryConfig; +import io.airbyte.integrations.base.IntegrationRunner; +import io.airbyte.integrations.base.Source; +import io.airbyte.integrations.source.jdbc.AbstractJdbcSource; +import io.airbyte.integrations.source.jdbc.JdbcDataSourceUtils; +import io.airbyte.integrations.source.relationaldb.TableInfo; +import io.airbyte.protocol.models.CommonField; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TeradataSource extends AbstractJdbcSource implements Source { + + private static final Logger LOGGER = LoggerFactory.getLogger(TeradataSource.class); + + private static final int INTERMEDIATE_STATE_EMISSION_FREQUENCY = 10_000; + + static final String DRIVER_CLASS = "com.teradata.jdbc.TeraDriver"; + + public static final String PARAM_MODE = "mode"; + public static final String PARAM_SSL = "ssl"; + public static final String PARAM_SSL_MODE = "ssl_mode"; + public static final String PARAM_SSLMODE = "sslmode"; + public static final String PARAM_SSLCA = "sslca"; + public static final String REQUIRE = "require"; + + private static final String CA_CERTIFICATE = "ca.pem"; + + public TeradataSource() { + super(DRIVER_CLASS, AdaptiveStreamingQueryConfig::new, new TeradataSourceOperations()); + } + + public static void main(final String[] args) throws Exception { + final Source source = new TeradataSource(); + LOGGER.info("starting source: {}", TeradataSource.class); + new IntegrationRunner(source).run(args); + LOGGER.info("completed source: {}", TeradataSource.class); + } + + @Override + public JsonNode toDatabaseConfig(final JsonNode config) { + final String schema = config.get(JdbcUtils.DATABASE_KEY).asText(); + + final String host = config.has(JdbcUtils.PORT_KEY) ? + config.get(JdbcUtils.HOST_KEY).asText() + ":" + config.get(JdbcUtils.PORT_KEY).asInt() : + config.get(JdbcUtils.HOST_KEY).asText(); + + final String jdbcUrl = String.format("jdbc:teradata://%s/", host); + + final ImmutableMap.Builder configBuilder = ImmutableMap.builder() + .put(JdbcUtils.USERNAME_KEY, config.get(JdbcUtils.USERNAME_KEY).asText()) + .put(JdbcUtils.JDBC_URL_KEY, jdbcUrl) + .put(JdbcUtils.SCHEMA_KEY, schema); + + if (config.has(JdbcUtils.PASSWORD_KEY)) { + configBuilder.put(JdbcUtils.PASSWORD_KEY, config.get(JdbcUtils.PASSWORD_KEY).asText()); + } + + if (config.has(JdbcUtils.JDBC_URL_PARAMS_KEY)) { + configBuilder.put(JdbcUtils.JDBC_URL_PARAMS_KEY, config.get(JdbcUtils.JDBC_URL_PARAMS_KEY).asText()); + } + + return Jsons.jsonNode(configBuilder.build()); + } + + @Override + public Set getExcludedInternalNameSpaces() { + // the connector requires to have a database explicitly defined + return Set.of(""); + } + + @Override + protected int getStateEmissionFrequency() { + return INTERMEDIATE_STATE_EMISSION_FREQUENCY; + } + + @Override + public List>> discoverInternal(JdbcDatabase database) throws Exception { + return discoverInternal(database, database.getSourceConfig().has(JdbcUtils.DATABASE_KEY) ? + database.getSourceConfig().get(JdbcUtils.DATABASE_KEY).asText() : null); + } + + @Override + public JdbcDatabase createDatabase(JsonNode sourceConfig) throws SQLException { + final Map customProperties = JdbcUtils.parseJdbcParameters(sourceConfig, JdbcUtils.JDBC_URL_PARAMS_KEY); + final Map sslConnectionProperties = getSslConnectionProperties(sourceConfig); + JdbcDataSourceUtils.assertCustomParametersDontOverwriteDefaultParameters(customProperties, sslConnectionProperties); + + final JsonNode jdbcConfig = toDatabaseConfig(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, + driverClass, + jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText(), + MoreMaps.merge(customProperties, sslConnectionProperties)); + // Record the data source so that it can be closed. + dataSources.add(dataSource); + + final JdbcDatabase database = new StreamingJdbcDatabase( + dataSource, + sourceOperations, + streamingQueryConfigProvider); + + quoteString = (quoteString == null ? database.getMetaData().getIdentifierQuoteString() : quoteString); + database.setSourceConfig(sourceConfig); + database.setDatabaseConfig(jdbcConfig); + return database; + } + + private Map getSslConnectionProperties(JsonNode config) { + final Map additionalParameters = new HashMap<>(); + if (config.has(PARAM_SSL) && config.get(PARAM_SSL).asBoolean()) { + LOGGER.debug("SSL Enabled"); + if (config.has(PARAM_SSL_MODE)) { + LOGGER.debug("Selected SSL Mode : {}", config.get(PARAM_SSL_MODE).get(PARAM_MODE).asText()); + additionalParameters.putAll(obtainConnectionOptions(config.get(PARAM_SSL_MODE))); + } else { + additionalParameters.put(PARAM_SSLMODE, REQUIRE); + } + } + return additionalParameters; + } + + private Map obtainConnectionOptions(final JsonNode encryption) { + final Map additionalParameters = new HashMap<>(); + if (!encryption.isNull()) { + final var method = encryption.get(PARAM_MODE).asText(); + switch (method) { + case "verify-ca", "verify-full" -> { + additionalParameters.put(PARAM_SSLMODE, method); + try { + createCertificateFile(CA_CERTIFICATE, encryption.get("ssl_ca_certificate").asText()); + } catch (final IOException ioe) { + throw new UncheckedIOException(ioe); + } + additionalParameters.put(PARAM_SSLCA, CA_CERTIFICATE); + } + default -> additionalParameters.put(PARAM_SSLMODE, method); + } + } + return additionalParameters; + } + + private static void createCertificateFile(String fileName, String fileValue) throws IOException { + try (final PrintWriter out = new PrintWriter(fileName, StandardCharsets.UTF_8)) { + out.print(fileValue); + } + } + + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSourceOperations.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSourceOperations.java new file mode 100644 index 000000000000..b600ec25103e --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/TeradataSourceOperations.java @@ -0,0 +1,114 @@ +package io.airbyte.integrations.source.teradata; + +import static io.airbyte.db.DataTypeUtils.TIMESTAMPTZ_FORMATTER; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.db.DataTypeUtils; +import io.airbyte.db.jdbc.DateTimeConverter; +import io.airbyte.db.jdbc.JdbcSourceOperations; +import java.sql.Date; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.text.ParseException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; + +// Teradata only supports native java.sql types when creating prepared statements and returning from resultSet +public class TeradataSourceOperations extends JdbcSourceOperations { + + @Override + protected void putDate(ObjectNode node, String columnName, ResultSet resultSet, int index) + throws SQLException { + Object obj = resultSet.getObject(index); + node.put(columnName, DateTimeConverter.convertToDate(obj)); + } + + @Override + protected void putTime(ObjectNode node, String columnName, ResultSet resultSet, int index) + throws SQLException { + Object obj = resultSet.getObject(index); + node.put(columnName, DateTimeConverter.convertToTime(obj)); + } + + @Override + protected void putTimestamp(ObjectNode node, String columnName, ResultSet resultSet, + int index) throws SQLException { + Timestamp timestamp = (Timestamp) resultSet.getObject(index); + node.put(columnName, DataTypeUtils.toISO8601StringWithMicroseconds(timestamp.toInstant())); + } + + @Override + protected void putTimestampWithTimezone(ObjectNode node, String columnName, ResultSet resultSet, + int index) throws SQLException { + Timestamp timestamp = (Timestamp) resultSet.getObject(index); + final OffsetDateTime timestamptz = timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC); + final LocalDate localDate = timestamptz.toLocalDate(); + node.put(columnName, resolveEra(localDate, timestamptz.format(TIMESTAMPTZ_FORMATTER))); + } + + + @Override + protected void setDate(PreparedStatement preparedStatement, int parameterIndex, String value) + throws SQLException { + try { + // LocalDate is unsupported by the Teradata driver if provided directly + preparedStatement.setObject(parameterIndex, Date.valueOf(LocalDate.parse(value))); + } catch (final DateTimeParseException dtpe) { + try { + final Timestamp from = Timestamp.from(DataTypeUtils.getDateFormat().parse(value).toInstant()); + preparedStatement.setDate(parameterIndex, new Date(from.getTime())); + } catch (final ParseException pe) { + throw new RuntimeException(pe); + } + } + } + + @Override + protected void setTime(PreparedStatement preparedStatement, int parameterIndex, String value) + throws SQLException { + try { + // LocalTime is unsupported by the Teradata driver if provided directly + preparedStatement.setObject(parameterIndex, Time.valueOf(LocalTime.parse(value))); + } catch (final DateTimeParseException e) { + setTimestamp(preparedStatement, parameterIndex, value); + } + } + + @Override + protected void setTimestamp(PreparedStatement preparedStatement, int parameterIndex, String value) + throws SQLException { + try { + preparedStatement.setObject(parameterIndex, Timestamp.valueOf(LocalDateTime.parse(value))); + } catch (final DateTimeParseException e) { + preparedStatement.setObject(parameterIndex, Timestamp.valueOf(OffsetDateTime.parse(value).toLocalDateTime())); + } + } + + @Override + protected void setTimeWithTimezone(PreparedStatement preparedStatement, int parameterIndex, + String value) throws SQLException { + try { + preparedStatement.setObject(parameterIndex, Time.valueOf(OffsetTime.parse(value).toLocalTime())); + } catch (final DateTimeParseException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void setTimestampWithTimezone(PreparedStatement preparedStatement, int parameterIndex, + String value) throws SQLException { + try { + preparedStatement.setObject(parameterIndex, Timestamp.valueOf(OffsetDateTime.parse(value).toLocalDateTime())); + } catch (final DateTimeParseException e) { + throw new RuntimeException(e); + } + } +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/Headers.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/Headers.java new file mode 100644 index 000000000000..48927e0c4862 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/Headers.java @@ -0,0 +1,18 @@ +package io.airbyte.integrations.source.teradata.envclient; + +public class Headers { + + private Headers() { + + } + + public static final String CONTENT_TYPE = "Content-Type"; + + public static final String AUTHORIZATION = "Authorization"; + + public static final String APPLICATION_JSON = "application/json"; + + public static final String BEARER = "Bearer "; + + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/TeradataHttpClient.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/TeradataHttpClient.java new file mode 100644 index 000000000000..e7e80163e2c4 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/TeradataHttpClient.java @@ -0,0 +1,137 @@ +package io.airbyte.integrations.source.teradata.envclient; + +import static io.airbyte.integrations.source.teradata.envclient.Headers.APPLICATION_JSON; +import static io.airbyte.integrations.source.teradata.envclient.Headers.AUTHORIZATION; +import static io.airbyte.integrations.source.teradata.envclient.Headers.BEARER; +import static io.airbyte.integrations.source.teradata.envclient.Headers.CONTENT_TYPE; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import io.airbyte.integrations.source.teradata.envclient.dto.CreateEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.dto.DeleteEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.dto.EnvironmentResponse; +import io.airbyte.integrations.source.teradata.envclient.dto.GetEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.exception.BaseException; +import io.airbyte.integrations.source.teradata.envclient.exception.Error4xxException; +import io.airbyte.integrations.source.teradata.envclient.exception.Error5xxException; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; + +public class TeradataHttpClient { + + private final String baseUrl; + + private final HttpClient httpClient; + + private final ObjectMapper objectMapper; + + public TeradataHttpClient(String baseUrl) { + this(HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build(), baseUrl); + } + + public TeradataHttpClient(HttpClient httpClient, String baseUrl) { + this.httpClient = httpClient; + this.baseUrl = baseUrl; + this.objectMapper = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS, false) + .build(); + } + + // Creating an environment is a blocking operation by default, and it takes ~1.5min to finish + public CompletableFuture createEnvironment(CreateEnvironmentRequest createEnvironmentRequest, + String token) { + var requestBody = handleCheckedException(() -> objectMapper.writeValueAsString(createEnvironmentRequest)); + + var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl.concat("/environments"))) + .headers( + AUTHORIZATION, BEARER + token, + CONTENT_TYPE, APPLICATION_JSON + ) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> handleHttpResponse(httpResponse, new TypeReference<>() {})); + } + + // Avoids long connections and the risk of connection termination by intermediary + public CompletableFuture pollingCreateEnvironment( + CreateEnvironmentRequest createEnvironmentRequest, String token) { + throw new UnsupportedOperationException(); + } + + public EnvironmentResponse getEnvironment(GetEnvironmentRequest getEnvironmentRequest, String token) { + var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl + .concat("/environments/") + .concat(getEnvironmentRequest.name()))) + .headers(AUTHORIZATION, BEARER + token) + .GET() + .build(); + + var httpResponse = + handleCheckedException(() -> httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString())); + return handleHttpResponse(httpResponse, new TypeReference<>() {}); + } + + // Deleting an environment is a blocking operation by default, and it takes ~1.5min to finish + public CompletableFuture deleteEnvironment(DeleteEnvironmentRequest deleteEnvironmentRequest, String token) { + var httpRequest = HttpRequest.newBuilder(URI.create(baseUrl + .concat("/environments/") + .concat(deleteEnvironmentRequest.name()))) + .headers(AUTHORIZATION, BEARER + token) + .DELETE() + .build(); + + return httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofString()) + .thenApply(httpResponse -> handleHttpResponse(httpResponse, new TypeReference<>() {})); + } + + + private T handleHttpResponse(HttpResponse httpResponse, TypeReference typeReference) { + var body = httpResponse.body(); + if (httpResponse.statusCode() >= 200 && httpResponse.statusCode() <= 299) { + return handleCheckedException(() -> { + if (typeReference.getType().getTypeName().equals(Void.class.getTypeName())) { + return null; + } else { + return objectMapper.readValue(body, typeReference); + } + }); + } else if (httpResponse.statusCode() >= 400 && httpResponse.statusCode() <= 499) { + throw new Error4xxException(httpResponse.statusCode(), body); + } else if (httpResponse.statusCode() >= 500 && httpResponse.statusCode() <= 599) { + throw new Error5xxException(httpResponse.statusCode(), body); + } else { + throw new BaseException(httpResponse.statusCode(), body); + } + } + + private T handleCheckedException(CheckedSupplier checkedSupplier) { + try { + return checkedSupplier.get(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + + @FunctionalInterface + private interface CheckedSupplier { + + T get() throws IOException, InterruptedException; + + } + + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/CreateEnvironmentRequest.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/CreateEnvironmentRequest.java new file mode 100644 index 000000000000..d5f329dce64f --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/CreateEnvironmentRequest.java @@ -0,0 +1,13 @@ +package io.airbyte.integrations.source.teradata.envclient.dto; + +public record CreateEnvironmentRequest( + + String name, + + String region, + + String password + +) { + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/DeleteEnvironmentRequest.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/DeleteEnvironmentRequest.java new file mode 100644 index 000000000000..24546efa003b --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/DeleteEnvironmentRequest.java @@ -0,0 +1,8 @@ +package io.airbyte.integrations.source.teradata.envclient.dto; + +public record DeleteEnvironmentRequest( + + String name + +) { +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/EnvironmentResponse.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/EnvironmentResponse.java new file mode 100644 index 000000000000..6943567e364f --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/EnvironmentResponse.java @@ -0,0 +1,55 @@ +package io.airbyte.integrations.source.teradata.envclient.dto; + +import java.util.List; + +public record EnvironmentResponse( + + State state, + + String region, + + // Use for subsequent environment operations i.e GET, DELETE, etc + String name, + + // Use for connecting with JDBC driver + String ip, + + String dnsName, + + String owner, + + String type, + + List services + +) { + + record Service( + + List credentials, + + String name, + + String url + + ) { + + } + + record Credential( + + String name, + + String value + + ) { + + } + + enum State { + + INITIALIZING, RUNNING, STOPPING, TERMINATING, + + } + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/GetEnvironmentRequest.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/GetEnvironmentRequest.java new file mode 100644 index 000000000000..7d8898f76ae1 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/GetEnvironmentRequest.java @@ -0,0 +1,8 @@ +package io.airbyte.integrations.source.teradata.envclient.dto; + +public record GetEnvironmentRequest( + + String name + +) { +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/Region.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/Region.java new file mode 100644 index 000000000000..9e516ed8b843 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/dto/Region.java @@ -0,0 +1,33 @@ +package io.airbyte.integrations.source.teradata.envclient.dto; + +public enum Region { + + US_CENTRAL("us-central"), + + US_EAST("us-east"), + + US_WEST("us-west"), + + SOUTHAMERICA_EAST("southamerica-east"), + + EUROPE_WEST("europe-west"), + + ASIA_SOUTH("asia-south"), + + ASIA_NORTHEAST("asia-northeast"), + + ASIA_SOUTHEAST("asia-southeast"), + + AUSTRALIA_SOUTHEAST("australia-southeast"); + + + private final String regionName; + + Region(String regionName) { + this.regionName = regionName; + } + + public String getRegionName() { + return regionName; + } +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/BaseException.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/BaseException.java new file mode 100644 index 000000000000..6ca0aaff2d07 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/BaseException.java @@ -0,0 +1,36 @@ +package io.airbyte.integrations.source.teradata.envclient.exception; + +public class BaseException extends RuntimeException { + + private final int statusCode; + + private final String body; + + private final String reason; + + public BaseException(int statusCode, String body) { + super(body); + this.statusCode = statusCode; + this.body = body; + this.reason = null; + } + + public BaseException(int statusCode, String body, String reason) { + super(body); + this.statusCode = statusCode; + this.body = body; + this.reason = reason; + } + + public int getStatusCode() { + return statusCode; + } + + public String getBody() { + return body; + } + + public String getReason() { + return reason; + } +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error4xxException.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error4xxException.java new file mode 100644 index 000000000000..1621ce3675d9 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error4xxException.java @@ -0,0 +1,13 @@ +package io.airbyte.integrations.source.teradata.envclient.exception; + +public class Error4xxException extends BaseException { + + public Error4xxException(int statusCode, String body, String reason) { + super(statusCode, body, reason); + } + + public Error4xxException(int statusCode, String body) { + super(statusCode, body); + } + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error5xxException.java b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error5xxException.java new file mode 100644 index 000000000000..4a7a6baf33c9 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/java/io/airbyte/integrations/source/teradata/envclient/exception/Error5xxException.java @@ -0,0 +1,13 @@ +package io.airbyte.integrations.source.teradata.envclient.exception; + +public class Error5xxException extends BaseException { + + public Error5xxException(int statusCode, String body, String reason) { + super(statusCode, body, reason); + } + + public Error5xxException(int statusCode, String body) { + super(statusCode, body); + } + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/main/resources/spec.json b/airbyte-integrations/connectors/source-teradata/src/main/resources/spec.json new file mode 100644 index 000000000000..25603e4e7330 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/main/resources/spec.json @@ -0,0 +1,192 @@ +{ + "documentationUrl" : "https://docs.airbyte.com/integrations/sources/teradata", + "connectionSpecification" : { + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "Teradata Source Spec", + "type" : "object", + "required" : [ + "host", + "database", + "username" + ], + "properties" : { + "host" : { + "title" : "Host", + "description" : "Hostname of the database.", + "type" : "string", + "order" : 0 + }, + "port" : { + "title" : "Port", + "description" : "Port of the database.", + "type" : "integer", + "minimum" : 0, + "maximum" : 65536, + "default" : 3306, + "examples" : [ + "3306" + ], + "order" : 1 + }, + "database" : { + "title" : "Database", + "description" : "Name of the database.", + "type" : "string", + "order" : 2 + }, + "username" : { + "title" : "Username", + "description" : "Username to use to access the database.", + "type" : "string", + "order" : 3 + }, + "password" : { + "title" : "Password", + "description" : "Password associated with the username.", + "type" : "string", + "airbyte_secret" : true, + "order" : 4 + }, + "jdbc_url_params" : { + "title" : "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)", + "type" : "string", + "order" : 5 + }, + "replication_method" : { + "title" : "Replication method", + "description" : "Replication method to use for extracting data from the database. STANDARD replication requires no setup on the DB side but will not be able to represent deletions incrementally. CDC uses the Binlog to detect inserts, updates, and deletes. This needs to be configured on the source database itself.", + "type" : "string", + "order" : 6, + "default" : "STANDARD", + "enum" : [ + "STANDARD", + "CDC" + ] + }, + "ssl" : { + "title" : "SSL Connection", + "description" : "Encrypt data using SSL. When activating SSL, please select one of the connection modes.", + "type" : "boolean", + "default" : false, + "order" : 7 + }, + "ssl_mode" : { + "title" : "SSL Modes", + "description" : "SSL connection modes. \n disable - Chose this mode to disable encryption of communication between Airbyte and destination database\n allow - Chose this mode to enable encryption only when required by the destination database\n prefer - Chose this mode to allow unencrypted connection only if the destination database does not support encryption\n require - Chose this mode to always require encryption. If the destination database server does not support encryption, connection will fail\n verify-ca - Chose this mode to always require encryption and to verify that the destination database server has a valid SSL certificate\n verify-full - This is the most secure mode. Chose this mode to always require encryption and to verify the identity of the destination database server\n See more information - in the docs.", + "type" : "object", + "order" : 8, + "oneOf" : [ + { + "title" : "disable", + "additionalProperties" : true, + "description" : "Disable SSL.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "disable", + "order" : 0 + } + } + }, + { + "title" : "allow", + "additionalProperties" : true, + "description" : "Allow SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "allow", + "order" : 0 + } + } + }, + { + "title" : "prefer", + "additionalProperties" : true, + "description" : "Prefer SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "prefer", + "order" : 0 + } + } + }, + { + "title" : "require", + "additionalProperties" : true, + "description" : "Require SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "require", + "order" : 0 + } + } + }, + { + "title" : "verify-ca", + "additionalProperties" : true, + "description" : "Verify-ca SSL mode.", + "required" : [ + "mode", + "ssl_ca_certificate" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "verify-ca", + "order" : 0 + }, + "ssl_ca_certificate" : { + "type" : "string", + "title" : "CA certificate", + "description" : "Specifies the file name of a PEM file that contains Certificate Authority (CA) certificates for use with SSLMODE=verify-ca.\n See more information - in the docs.", + "airbyte_secret" : true, + "multiline" : true, + "order" : 1 + } + } + }, + { + "title" : "verify-full", + "additionalProperties" : true, + "description" : "Verify-full SSL mode.", + "required" : [ + "mode", + "ssl_ca_certificate" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "verify-full", + "order" : 0 + }, + "ssl_ca_certificate" : { + "type" : "string", + "title" : "CA certificate", + "description" : "Specifies the file name of a PEM file that contains Certificate Authority (CA) certificates for use with SSLMODE=verify-full.\n See more information - in the docs.", + "airbyte_secret" : true, + "multiline" : true, + "order" : 1 + } + } + } + ] + } + } + } +} diff --git a/airbyte-integrations/connectors/source-teradata/src/test-integration/java/io/airbyte/integrations/source/teradata/TeradataSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-teradata/src/test-integration/java/io/airbyte/integrations/source/teradata/TeradataSourceAcceptanceTest.java new file mode 100644 index 000000000000..607ce7764284 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/test-integration/java/io/airbyte/integrations/source/teradata/TeradataSourceAcceptanceTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.source.teradata; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.commons.io.IOs; +import io.airbyte.commons.json.Jsons; +import io.airbyte.commons.resources.MoreResources; +import io.airbyte.integrations.source.teradata.envclient.TeradataHttpClient; +import io.airbyte.integrations.source.teradata.envclient.dto.CreateEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.dto.DeleteEnvironmentRequest; +import io.airbyte.integrations.standardtest.source.SourceAcceptanceTest; +import io.airbyte.integrations.standardtest.source.TestDestinationEnv; +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.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ExecutionException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class TeradataSourceAcceptanceTest extends SourceAcceptanceTest { + + private static final String CREATE_DATABASE = + "CREATE DATABASE \"database_name\" AS PERMANENT = 120e6, SPOOL = 120e6;"; + + private static final String CREATE_TABLE = """ + CREATE TABLE database_name.table_name( + id INTEGER NOT NULL, + strength VARCHAR(30) NOT NULL, + agility INTEGER NOT NULL, + updated_at TIMESTAMP(6), + PRIMARY KEY(id)) + """; + + private static final String DELETE_DATABASE = "DELETE DATABASE \"database_name\";"; + + private static final String DROP_DATABASE = "DROP DATABASE \"database_name\";"; + + private static final String INSERT = + "INSERT INTO database_name.table_name VALUES(%d, '%s', %d, CURRENT_TIMESTAMP(6))"; + + private JsonNode jsonConfig; + + @BeforeAll + void initEnvironment() throws ExecutionException, InterruptedException { + jsonConfig = Jsons.deserialize(IOs.readFile(Path.of("secrets/config.json"))); + TeradataHttpClient teradataHttpClient = new TeradataHttpClient(jsonConfig.get("env_host").asText()); + var request = new CreateEnvironmentRequest( + jsonConfig.get("env_name").asText(), + jsonConfig.get("env_region").asText(), + jsonConfig.get("env_password").asText()); + var response = teradataHttpClient.createEnvironment(request, jsonConfig.get("env_token").asText()).get(); + ((ObjectNode) jsonConfig).put("host", response.ip()); + try { + Class.forName("com.teradata.jdbc.TeraDriver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @AfterAll + void cleanupEnvironment() throws ExecutionException, InterruptedException { + TeradataHttpClient teradataHttpClient = new TeradataHttpClient(jsonConfig.get("env_host").asText()); + var request = new DeleteEnvironmentRequest(jsonConfig.get("env_name").asText()); + teradataHttpClient.deleteEnvironment(request, jsonConfig.get("env_token").asText()).get(); + } + + + @Override + protected void setupEnvironment(final TestDestinationEnv testEnv) { + var config = getConfig(); + executeStatements(List.of( + statement -> statement.executeUpdate(CREATE_DATABASE), + statement -> statement.executeUpdate(CREATE_TABLE), + statement -> statement.executeUpdate(String.format(INSERT, 1, "laser power", 9)), + statement -> statement.executeUpdate(String.format(INSERT, 2, "night vision", 7)) + ), config.get("host").asText(), config.get("username").asText(), config.get("password").asText()); + + } + + @Override + protected void tearDown(final TestDestinationEnv testEnv) { + var config = getConfig(); + executeStatements(List.of( + statement -> statement.executeUpdate(DELETE_DATABASE), + statement -> statement.executeUpdate(DROP_DATABASE) + ), config.get("host").asText(), config.get("username").asText(), config.get("password").asText()); + } + + @Override + protected String getImageName() { + return "airbyte/source-teradata:dev"; + } + + @Override + protected ConnectorSpecification getSpec() throws Exception { + return Jsons.deserialize(MoreResources.readResource("spec.json"), ConnectorSpecification.class); + } + + @Override + protected JsonNode getConfig() { + return Jsons.clone(jsonConfig); + } + + @Override + protected ConfiguredAirbyteCatalog getConfiguredCatalog() { + return createConfiguredAirbyteCatalog(); + } + + @Override + protected JsonNode getState() { + return Jsons.jsonNode(new HashMap<>()); + } + + + static ConfiguredAirbyteCatalog createConfiguredAirbyteCatalog() { + return new ConfiguredAirbyteCatalog().withStreams(List.of( + new ConfiguredAirbyteStream() + .withSyncMode(SyncMode.INCREMENTAL) + .withCursorField(List.of("updated_at")) + .withPrimaryKey(List.of(List.of("id"))) + .withDestinationSyncMode(DestinationSyncMode.APPEND) + .withStream(CatalogHelpers.createAirbyteStream( + "table_name", + "database_name", + Field.of("strength", JsonSchemaType.STRING), + Field.of("agility", JsonSchemaType.INTEGER), + Field.of("updated_at", JsonSchemaType.TIMESTAMP_WITH_TIMEZONE_V1)) + .withSupportedSyncModes(List.of(SyncMode.FULL_REFRESH, SyncMode.INCREMENTAL))))); + } + + private static void executeStatements(List consumers, String host, String username, String password) { + try ( + Connection con = DriverManager.getConnection("jdbc:teradata://" + host + "/", username, password); + Statement stmt = con.createStatement(); + ) { + for (SqlConsumer consumer : consumers) { + consumer.accept(stmt); + } + } catch (SQLException sqle) { + throw new RuntimeException(sqle); + } + + } + + @FunctionalInterface + private interface SqlConsumer { + + void accept(Statement statement) throws SQLException; + + } + +} diff --git a/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/dummy_config.json b/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/dummy_config.json new file mode 100644 index 000000000000..ba363e290d7f --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/dummy_config.json @@ -0,0 +1,5 @@ +{ + "host": "127.0.0.1", + "database": "database_name", + "username": "demo_user" +} diff --git a/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/expected_spec.json b/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/expected_spec.json new file mode 100644 index 000000000000..774f80d71932 --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/test-integration/resources/expected_spec.json @@ -0,0 +1,193 @@ +{ + "documentationUrl" : "https://docs.airbyte.com/integrations/sources/teradata", + "connectionSpecification" : { + "$schema" : "http://json-schema.org/draft-07/schema#", + "title" : "Teradata Source Spec", + "type" : "object", + "required" : [ + "host", + "database", + "username" + ], + "properties" : { + "host" : { + "title" : "Host", + "description" : "Hostname of the database.", + "type" : "string", + "order" : 0 + }, + "port" : { + "title" : "Port", + "description" : "Port of the database.", + "type" : "integer", + "minimum" : 0, + "maximum" : 65536, + "default" : 3306, + "examples" : [ + "3306" + ], + "order" : 1 + }, + "database" : { + "title" : "Database", + "description" : "Name of the database.", + "type" : "string", + "order" : 2 + }, + "username" : { + "title" : "Username", + "description" : "Username to use to access the database.", + "type" : "string", + "order" : 3 + }, + "password" : { + "title" : "Password", + "description" : "Password associated with the username.", + "type" : "string", + "airbyte_secret" : true, + "order" : 4 + }, + "jdbc_url_params" : { + "title" : "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)", + "type" : "string", + "order" : 5 + }, + "replication_method" : { + "title" : "Replication method", + "description" : "Replication method to use for extracting data from the database. STANDARD replication requires no setup on the DB side but will not be able to represent deletions incrementally. CDC uses the Binlog to detect inserts, updates, and deletes. This needs to be configured on the source database itself.", + "type" : "string", + "order" : 6, + "default" : "STANDARD", + "enum" : [ + "STANDARD", + "CDC" + ] + }, + "ssl" : { + "title" : "SSL Connection", + "description" : "Encrypt data using SSL. When activating SSL, please select one of the connection modes.", + "type" : "boolean", + "default" : false, + "order" : 7 + }, + "ssl_mode" : { + "title" : "SSL Modes", + "description" : "SSL connection modes. \n disable - Chose this mode to disable encryption of communication between Airbyte and destination database\n allow - Chose this mode to enable encryption only when required by the destination database\n prefer - Chose this mode to allow unencrypted connection only if the destination database does not support encryption\n require - Chose this mode to always require encryption. If the destination database server does not support encryption, connection will fail\n verify-ca - Chose this mode to always require encryption and to verify that the destination database server has a valid SSL certificate\n verify-full - This is the most secure mode. Chose this mode to always require encryption and to verify the identity of the destination database server\n See more information - in the docs.", + "type" : "object", + "order" : 8, + "oneOf" : [ + { + "title" : "disable", + "additionalProperties" : true, + "description" : "Disable SSL.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "disable", + "order" : 0 + } + } + }, + { + "title" : "allow", + "additionalProperties" : true, + "description" : "Allow SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "allow", + "order" : 0 + } + } + }, + { + "title" : "prefer", + "additionalProperties" : true, + "description" : "Prefer SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "prefer", + "order" : 0 + } + } + }, + { + "title" : "require", + "additionalProperties" : true, + "description" : "Require SSL mode.", + "required" : [ + "mode" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "require", + "order" : 0 + } + } + }, + { + "title" : "verify-ca", + "additionalProperties" : true, + "description" : "Verify-ca SSL mode.", + "required" : [ + "mode", + "ssl_ca_certificate" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "verify-ca", + "order" : 0 + }, + "ssl_ca_certificate" : { + "type" : "string", + "title" : "CA certificate", + "description" : "Specifies the file name of a PEM file that contains Certificate Authority (CA) certificates for use with SSLMODE=verify-ca.\n See more information - in the docs.", + "airbyte_secret" : true, + "multiline" : true, + "order" : 1 + } + } + }, + { + "title" : "verify-full", + "additionalProperties" : true, + "description" : "Verify-full SSL mode.", + "required" : [ + "mode", + "ssl_ca_certificate" + ], + "properties" : { + "mode" : { + "type" : "string", + "const" : "verify-full", + "order" : 0 + }, + "ssl_ca_certificate" : { + "type" : "string", + "title" : "CA certificate", + "description" : "Specifies the file name of a PEM file that contains Certificate Authority (CA) certificates for use with SSLMODE=verify-full.\n See more information - in the docs.", + "airbyte_secret" : true, + "multiline" : true, + "order" : 1 + } + } + } + ] + } + } + }, + "supported_destination_sync_modes" : [] +} diff --git a/airbyte-integrations/connectors/source-teradata/src/test/java/io/airbyte/integrations/source/teradata/TeradataJdbcSourceAcceptanceTest.java b/airbyte-integrations/connectors/source-teradata/src/test/java/io/airbyte/integrations/source/teradata/TeradataJdbcSourceAcceptanceTest.java new file mode 100644 index 000000000000..8acae55328bb --- /dev/null +++ b/airbyte-integrations/connectors/source-teradata/src/test/java/io/airbyte/integrations/source/teradata/TeradataJdbcSourceAcceptanceTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2023 Airbyte, Inc., all rights reserved. + */ + +package io.airbyte.integrations.source.teradata; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.airbyte.commons.io.IOs; +import io.airbyte.commons.json.Jsons; +import io.airbyte.integrations.source.jdbc.AbstractJdbcSource; +import io.airbyte.integrations.source.jdbc.test.JdbcSourceAcceptanceTest; +import io.airbyte.integrations.source.teradata.envclient.TeradataHttpClient; +import io.airbyte.integrations.source.teradata.envclient.dto.CreateEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.dto.DeleteEnvironmentRequest; +import io.airbyte.integrations.source.teradata.envclient.dto.Region; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TeradataJdbcSourceAcceptanceTest extends JdbcSourceAcceptanceTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(TeradataJdbcSourceAcceptanceTest.class); + + private JsonNode staticConfig; + + static { + COLUMN_CLAUSE_WITH_PK = "id INTEGER NOT NULL, name VARCHAR(200) NOT NULL, updated_at DATE NOT NULL"; + + CREATE_TABLE_WITHOUT_CURSOR_TYPE_QUERY = "CREATE TABLE %s (%s ST_Geometry) NO PRIMARY INDEX;"; + INSERT_TABLE_WITHOUT_CURSOR_TYPE_QUERY = "INSERT INTO %s VALUES('POLYGON((1 1, 1 3, 6 3, 6 0, 1 1))');"; + + COL_TIMESTAMP = "tmstmp"; + INSERT_TABLE_NAME_AND_TIMESTAMP_QUERY = "INSERT INTO %s (name, tmstmp) VALUES ('%s', '%s')"; + COL_TIMESTAMP_TYPE = "TIMESTAMP(0)"; + } + + + @BeforeAll + public void initEnvironment() throws ExecutionException, InterruptedException { + staticConfig = Jsons.deserialize(IOs.readFile(Path.of("secrets/config.json"))); + TeradataHttpClient teradataHttpClient = new TeradataHttpClient(staticConfig.get("env_host").asText()); + var request = new CreateEnvironmentRequest( + staticConfig.get("env_name").asText(), + staticConfig.get("env_region").asText(), + staticConfig.get("env_password").asText()); + var response = teradataHttpClient.createEnvironment(request, staticConfig.get("env_token").asText()).get(); + ((ObjectNode) staticConfig).put("host", response.ip()); + try { + Class.forName("com.teradata.jdbc.TeraDriver"); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @AfterAll + public void cleanupEnvironment() throws ExecutionException, InterruptedException { + TeradataHttpClient teradataHttpClient = new TeradataHttpClient(staticConfig.get("env_host").asText()); + var request = new DeleteEnvironmentRequest(staticConfig.get("env_name").asText()); + teradataHttpClient.deleteEnvironment(request, staticConfig.get("env_token").asText()).get(); + } + + @BeforeEach + public void setup() throws Exception { + executeStatements(List.of( + statement -> statement.executeUpdate("CREATE DATABASE \"database_name\" AS PERMANENT = 120e6, SPOOL = 120e6;") + ), staticConfig.get("host").asText(), staticConfig.get("username").asText(), staticConfig.get("password").asText()); + super.setup(); + } + + @AfterEach + public void tearDown() { + executeStatements(List.of( + statement -> statement.executeUpdate("DELETE DATABASE \"database_name\";"), + statement -> statement.executeUpdate("DROP DATABASE \"database_name\";") + ), staticConfig.get("host").asText(), staticConfig.get("username").asText(), staticConfig.get("password").asText()); + } + + @Override + public AbstractJdbcSource getSource() { + return new TeradataSource(); + } + + @Override + public boolean supportsSchemas() { + // TODO check if your db supports it and update method accordingly + return false; + } + + @Override + public JsonNode getConfig() { + return Jsons.clone(staticConfig); + } + + @Override + public String getDriverClass() { + return TeradataSource.DRIVER_CLASS; + } + + @Override + public AbstractJdbcSource getJdbcSource() { + return new TeradataSource(); + } + + @Override + public String getFullyQualifiedTableName(String tableName) { + return "database_name." + tableName; + } + + private static void executeStatements(List consumers, String host, String username, String password) { + try ( + Connection con = DriverManager.getConnection("jdbc:teradata://" + host + "/", username, password); + Statement stmt = con.createStatement(); + ) { + for (SqlConsumer consumer : consumers) { + consumer.accept(stmt); + } + } catch (SQLException sqle) { + throw new RuntimeException(sqle); + } + + } + + @FunctionalInterface + private interface SqlConsumer { + + void accept(Statement statement) throws SQLException; + + } + +} diff --git a/docs/integrations/sources/teradata.md b/docs/integrations/sources/teradata.md new file mode 100644 index 000000000000..a8c83ab16789 --- /dev/null +++ b/docs/integrations/sources/teradata.md @@ -0,0 +1,66 @@ +# Teradata + +This page guides you through the process of setting up the Teradata source connector. + +## Prerequisites + +To use the Teradata source connector, you'll need: + +* Access to a Teradata Vantage instance + + **Note:** If you need a new instance of Vantage, you can install a free version called Vantage Express in the cloud on [Google Cloud](https://quickstarts.teradata.com/vantage.express.gcp.html), [Azure](https://quickstarts.teradata.com/run-vantage-express-on-microsoft-azure.html), and [AWS](https://quickstarts.teradata.com/run-vantage-express-on-aws.html). You can also run Vantage Express on your local machine using [VMware](https://quickstarts.teradata.com/getting.started.vmware.html), [VirtualBox](https://quickstarts.teradata.com/getting.started.vbox.html), or [UTM](https://quickstarts.teradata.com/getting.started.utm.html). + +You'll need the following information to configure the Teradata source: + +* **Host** - The host name of the Teradata Vantage instance. +* **Username** +* **Password** +* **Database** - Specify the database (equivalent to schema in some databases i.e. **database_name.table_name** when performing queries). +* **JDBC URL Params** (optional) +* **SSL Connection** (optional) +* **SSL Modes** (optional) + +[Refer to this guide for more details](https://downloads.teradata.com/doc/connectivity/jdbc/reference/current/jdbcug_chapter_2.html#BGBHDDGB) + +## Supported sync modes + +The Teradata source connector supports the following [sync modes](https://docs.airbyte.com/cloud/core-concepts#connection-sync-modes): + +| Feature | Supported? | +|:-----------------------------------------------|:-----------------------------------------------------------| +| Full Refresh Sync | Yes | +| Incremental Sync | Yes | +| Replicate Incremental Deletes | No | +| Replicate Multiple Streams \(distinct tables\) | Yes | +| Namespaces | No (separate connection is needed for different databases) | + +### Performance considerations + +## Getting started + +### Requirements + +You need a Teradata user which has read permissions on the database + +### Setup guide + +#### Set up the Teradata Source connector + +1. Log into your Airbyte Open Source account. +2. Click **Sources** and then click **+ New source**. +3. On the Set up the source page, select **Teradata** from the **Source type** dropdown. +4. Enter the **Name** for the Teradata connector. +5. For **Host**, enter the host domain of the Teradata instance +6. For **Database**, enter the database name (equivalent to schema in some other databases). +7. For **User** and **Password**, enter the database username and password. +8. To customize the JDBC connection beyond common options, specify additional supported [JDBC URL parameters](https://downloads.teradata.com/doc/connectivity/jdbc/reference/current/jdbcug_chapter_2.html#BGBHDDGB) as key-value pairs separated by the symbol & in the **JDBC URL Params** field. + + Example: key1=value1&key2=value2&key3=value3 + + These parameters will be added at the end of the JDBC URL that the AirByte will use to connect to your Teradata database. + +## CHANGELOG + +| Version | Date | Pull Request | Subject | +|:--------|:-----------|:------------------------------------------------|:----------------------------| +| 0.1.0 | 2022-03-27 | https://github.com/airbytehq/airbyte/pull/24221 | New Source Teradata Vantage | \ No newline at end of file