Skip to content

Commit 70af402

Browse files
🎉 New Destination: SingleStore (#38600)
Co-authored-by: Marcos Marx <[email protected]> Co-authored-by: marcosmarxm <[email protected]>
1 parent 93c02d7 commit 70af402

File tree

77 files changed

+2757
-0
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2757
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Destination SingleStore
2+
3+
This is the repository for the Singlestore destination connector in Java.
4+
For information about how to use this connector within Airbyte, see [the User Documentation](https://docs.airbyte.com/integrations/destinations/singlestore).
5+
6+
## Local development
7+
8+
#### Building via Gradle
9+
From the Airbyte repository root, run:
10+
```
11+
./gradlew :airbyte-integrations:connectors:destination-singlestore:build
12+
```
13+
14+
#### Create credentials
15+
**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`.
16+
Note that the `secrets` directory is git-ignored by default, so there is no danger of accidentally checking in sensitive information.
17+
18+
**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.
19+
20+
### Locally running the connector docker image
21+
22+
#### Build
23+
Build the connector image via Gradle:
24+
```
25+
./gradlew :airbyte-integrations:connectors:destination-singlestore:buildConnectorImage
26+
```
27+
Once built, the docker image name and tag will be `airbyte/source-singlestore:dev`.
28+
29+
30+
#### Run
31+
Then run any of the connector commands as follows:
32+
```
33+
docker run --rm airbyte/destination-singlestore:dev spec
34+
docker run --rm -v $(pwd)/secrets:/secrets airbyte/destination-singlestore:dev check --config /secrets/config.json
35+
docker run --rm -v $(pwd)/secrets:/secrets airbyte/destination-singlestore:dev discover --config /secrets/config.json
36+
docker run --rm -v $(pwd)/secrets:/secrets -v $(pwd)/integration_tests:/integration_tests airbyte/destination-singlestore:dev read --config /secrets/config.json --catalog /integration_tests/configured_catalog.json
37+
```
38+
39+
## Testing
40+
We use `JUnit` for Java tests.
41+
42+
To execute unit tests you need to set up the environment variable 'SINGLESTORE_LICENSE' with SingleStore license value.
43+
To get a free-tier license you need to create a free SingleStore account.
44+
More details:
45+
https://www.singlestore.com/blog/announcing-memsql-free-tier/
46+
47+
### Using gradle to run tests
48+
All commands should be run from airbyte project root.
49+
To run unit tests:
50+
```
51+
./gradlew :airbyte-integrations:connectors:destination-singlestore:check
52+
```
53+
To run acceptance and custom integration tests:
54+
```
55+
./gradlew :airbyte-integrations:connectors:destination-singlestore:integrationTest
56+
```
57+
58+
## Dependency Management
59+
60+
### Publishing a new version of the connector
61+
You've checked out the repo, implemented a million dollar feature, and you're ready to share your changes with the world. Now what?
62+
1. Make sure your changes are passing our test suite: `airbyte-ci connectors --name=destination-singlestore test`
63+
2. Bump the connector version in `metadata.yaml`: increment the `dockerImageTag` value. Please follow [semantic versioning for connectors](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#semantic-versioning-for-connectors).
64+
3. Make sure the `metadata.yaml` content is up to date.
65+
4. Make the connector documentation and its changelog is up to date (`docs/integrations/destinations/singlestore.md`).
66+
5. Create a Pull Request: use [our PR naming conventions](https://docs.airbyte.com/contributing-to-airbyte/resources/pull-requests-handbook/#pull-request-title-convention).
67+
6. Pat yourself on the back for being an awesome contributor.
68+
7. Someone from Airbyte will take a look at your PR and iterate with you to merge it into master.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
plugins {
2+
id 'application'
3+
id 'airbyte-java-connector'
4+
}
5+
6+
airbyteJavaConnector {
7+
cdkVersionRequired = '0.36.7'
8+
features = ['db-destinations', 'typing-deduping']
9+
useLocalCdk = false
10+
}
11+
12+
airbyteJavaConnector.addCdkDependencies()
13+
14+
application {
15+
mainClass = 'io.airbyte.integrations.destination.singlestore.SingleStoreDestination'
16+
applicationDefaultJvmArgs = ['-XX:+ExitOnOutOfMemoryError', '-XX:MaxRAMPercentage=75.0']
17+
}
18+
19+
dependencies {
20+
implementation 'com.singlestore:singlestore-jdbc-client:1.2.6'
21+
testImplementation 'org.apache.commons:commons-lang3:3.11'
22+
testImplementation 'org.testcontainers:jdbc:1.19.0'
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
testExecutionConcurrency=4
2+
# large sync test takes a while, add 15m timeout.
3+
JunitMethodExecutionTimeout=15 m
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
data:
2+
connectorSubtype: database
3+
connectorType: destination
4+
definitionId: 216d815c-8ddc-4617-8cb3-3a23e5811292
5+
dockerImageTag: 0.1.0
6+
dockerRepository: airbyte/destination-singlestore
7+
githubIssueLabel: destination-singlestore
8+
icon: icon.svg
9+
license: MIT
10+
name: SingleStore
11+
releaseDate:
12+
supportLevel: community
13+
releaseStage: alpha
14+
documentationUrl: https://docs.airbyte.com/integrations/destinations/singlestore
15+
tags:
16+
- language:java
17+
metadataSpecVersion: "1.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright (c) 2024 Airbyte, Inc., all rights reserved.
3+
*/
4+
5+
package io.airbyte.integrations.destination.singlestore;
6+
7+
import static io.airbyte.cdk.db.jdbc.JdbcUtils.parseJdbcParameters;
8+
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
import com.google.common.collect.ImmutableMap;
11+
import io.airbyte.cdk.db.factory.DataSourceFactory;
12+
import io.airbyte.cdk.db.factory.DatabaseDriver;
13+
import io.airbyte.cdk.db.jdbc.JdbcUtils;
14+
import io.airbyte.commons.json.Jsons;
15+
import io.airbyte.commons.map.MoreMaps;
16+
import java.time.Duration;
17+
import java.util.Map;
18+
import javax.sql.DataSource;
19+
import org.jetbrains.annotations.NotNull;
20+
21+
public final class SingleStoreConnectorFactory {
22+
23+
private static final String SINGLE_STORE_METRIC_NAME = "Airbyte Destination Connector";
24+
static final Map<String, String> DEFAULT_JDBC_PARAMETERS = ImmutableMap.of("allowLocalInfile", "true");
25+
static final Map<String, String> DEFAULT_SSL_JDBC_PARAMETERS = MoreMaps.merge(ImmutableMap.of("sslMode", "trust"), DEFAULT_JDBC_PARAMETERS);
26+
27+
private SingleStoreConnectorFactory() {}
28+
29+
public static DataSource createDataSource(JsonNode config) {
30+
final String jdbcUrl = String.format("jdbc:singlestore://%s:%s/%s?_connector_name=%s", config.get(JdbcUtils.HOST_KEY).asText(),
31+
config.get(JdbcUtils.PORT_KEY).asText(), config.get(JdbcUtils.DATABASE_KEY).asText(), SINGLE_STORE_METRIC_NAME);
32+
final ImmutableMap.Builder<Object, Object> configBuilder =
33+
ImmutableMap.builder().put(JdbcUtils.USERNAME_KEY, config.get(JdbcUtils.USERNAME_KEY).asText()).put(JdbcUtils.JDBC_URL_KEY, jdbcUrl);
34+
if (config.has(JdbcUtils.PASSWORD_KEY)) {
35+
configBuilder.put(JdbcUtils.PASSWORD_KEY, config.get(JdbcUtils.PASSWORD_KEY).asText());
36+
}
37+
if (config.has(JdbcUtils.JDBC_URL_PARAMS_KEY)) {
38+
configBuilder.put(JdbcUtils.JDBC_URL_PARAMS_KEY, config.get(JdbcUtils.JDBC_URL_PARAMS_KEY));
39+
}
40+
var jdbcConfig = Jsons.jsonNode(configBuilder.build());
41+
var connectionProperties = MoreMaps.merge(parseJdbcParameters(config, JdbcUtils.JDBC_URL_PARAMS_KEY), getDefaultConnectionProperties(config));
42+
var builder =
43+
new DataSourceFactory.DataSourceBuilder(
44+
jdbcConfig.get(JdbcUtils.USERNAME_KEY).asText(),
45+
jdbcConfig.has(JdbcUtils.PASSWORD_KEY) ? jdbcConfig.get(JdbcUtils.PASSWORD_KEY).asText() : null,
46+
DatabaseDriver.SINGLESTORE.getDriverClassName(),
47+
jdbcConfig.get(JdbcUtils.JDBC_URL_KEY).asText())
48+
.withConnectionProperties(connectionProperties);
49+
if (connectionProperties.get("connectTimeout") != null) {
50+
builder.withConnectionTimeout(Duration.ofMillis(Long.parseLong(connectionProperties.get("connectTimeout"))));
51+
}
52+
return modifyDataSourceBuilder(builder).build();
53+
}
54+
55+
private static Map<String, String> getDefaultConnectionProperties(@NotNull JsonNode config) {
56+
if (JdbcUtils.useSsl(config)) {
57+
return DEFAULT_SSL_JDBC_PARAMETERS;
58+
} else {
59+
return DEFAULT_JDBC_PARAMETERS;
60+
}
61+
}
62+
63+
private static DataSourceFactory.DataSourceBuilder modifyDataSourceBuilder(@NotNull DataSourceFactory.DataSourceBuilder builder) {
64+
return builder.withConnectionTimeout(Duration.ofSeconds(60))
65+
.withConnectionInitSql("""
66+
CREATE OR REPLACE FUNCTION can_cast(v VARCHAR(254), t VARCHAR(30)) RETURNS BOOL AS
67+
DECLARE
68+
v_pat VARCHAR(255) = CONCAT(v, "%");
69+
BEGIN
70+
IF v is NULL OR t = 'varchar' THEN
71+
RETURN TRUE;
72+
ELSIF t = 'bigint' THEN
73+
RETURN v !:> BIGINT !:> VARCHAR(255) = REPLACE(v, ' ', '');
74+
ELSIF t = 'date' THEN
75+
RETURN v !:> DATE !:> VARCHAR(255) = REPLACE(v, ' ', '');
76+
ELSIF t = 'timestamp' THEN
77+
RETURN v !:> TIMESTAMP(6) !:> VARCHAR(255) LIKE REGEXP_REPLACE(REPLACE(v_pat, 'T', ' '), 'z|Z', '');
78+
ELSIF t = 'time' THEN
79+
RETURN v !:> TIME(6) !:> VARCHAR(255) LIKE v_pat;
80+
ELSIF t = 'json' THEN
81+
RETURN (v:> VARCHAR(255) = '') OR (v !:> JSON IS NOT NULL);
82+
ELSIF t = 'decimal' THEN
83+
RETURN (v !:> DECIMAL(38, 9) !:> VARCHAR(255)) LIKE v_pat;
84+
ELSIF t = 'boolean' THEN
85+
RETURN UCASE(v) = 'TRUE' OR UCASE(v) = 'FALSE';
86+
ELSE
87+
RETURN FALSE;
88+
END IF;
89+
END
90+
""");
91+
}
92+
93+
}

0 commit comments

Comments
 (0)