Skip to content

Commit b9b8cb0

Browse files
itaseskiivincentkococtavia-squidington-iiisajarinsh4sh
authored
🐛 Source Dynamodb: Fix reserved words in expression (#20172)
* fix reserved words in expression * chore: bump version * fix state message * auto-bump connector version * fix reserved words in projection expression * bump dockerfile version * auto-bump connector version --------- Co-authored-by: Vincent Koc <[email protected]> Co-authored-by: Octavia Squidington III <[email protected]> Co-authored-by: Sajarin <[email protected]> Co-authored-by: Sunny <[email protected]>
1 parent 32ae1b0 commit b9b8cb0

File tree

10 files changed

+73
-23
lines changed

10 files changed

+73
-23
lines changed

airbyte-config/init/src/main/resources/seed/source_definitions.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@
474474
- name: DynamoDB
475475
sourceDefinitionId: 50401137-8871-4c5a-abb7-1f5fda35545a
476476
dockerRepository: airbyte/source-dynamodb
477-
dockerImageTag: 0.1.1
477+
dockerImageTag: 0.1.2
478478
documentationUrl: https://docs.airbyte.com/integrations/sources/dynamodb
479479
icon: dynamodb.svg
480480
sourceType: api

airbyte-config/init/src/main/resources/seed/source_specs.yaml

+8-1
Original file line numberDiff line numberDiff line change
@@ -3398,7 +3398,7 @@
33983398
supportsNormalization: false
33993399
supportsDBT: false
34003400
supported_destination_sync_modes: []
3401-
- dockerImage: "airbyte/source-dynamodb:0.1.1"
3401+
- dockerImage: "airbyte/source-dynamodb:0.1.2"
34023402
spec:
34033403
documentationUrl: "https://docs.airbyte.com/integrations/sources/dynamodb"
34043404
connectionSpecification:
@@ -3464,6 +3464,13 @@
34643464
airbyte_secret: true
34653465
examples:
34663466
- "a012345678910ABCDEFGH/AbCdEfGhEXAMPLEKEY"
3467+
reserved_attribute_names:
3468+
title: "Reserved attribute names"
3469+
type: "string"
3470+
description: "Comma separated reserved attribute names present in your tables"
3471+
airbyte_secret: true
3472+
examples:
3473+
- "name, field_name, field-name"
34673474
supportsNormalization: false
34683475
supportsDBT: false
34693476
supported_destination_sync_modes: []

airbyte-integrations/connectors/source-dynamodb/Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ ENV APPLICATION source-dynamodb
1717
COPY --from=build /airbyte /airbyte
1818

1919
# Airbyte's build system uses these labels to know what to name and tag the docker images produced by this Dockerfile.
20-
LABEL io.airbyte.version=0.1.1
20+
LABEL io.airbyte.version=0.1.2
2121
LABEL io.airbyte.name=airbyte/source-dynamodb

airbyte-integrations/connectors/source-dynamodb/src/main/java/io/airbyte/integrations/source/dynamodb/DynamodbConfig.java

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import com.fasterxml.jackson.databind.JsonNode;
88
import java.net.URI;
9+
import java.util.Arrays;
10+
import java.util.List;
911
import software.amazon.awssdk.regions.Region;
1012

1113
public record DynamodbConfig(
@@ -16,18 +18,22 @@ public record DynamodbConfig(
1618

1719
String accessKey,
1820

19-
String secretKey
21+
String secretKey,
22+
23+
List<String> reservedAttributeNames
2024

2125
) {
2226

2327
public static DynamodbConfig createDynamodbConfig(JsonNode jsonNode) {
2428
JsonNode endpoint = jsonNode.get("endpoint");
2529
JsonNode region = jsonNode.get("region");
30+
JsonNode attributeNames = jsonNode.get("reserved_attribute_names");
2631
return new DynamodbConfig(
2732
endpoint != null && !endpoint.asText().isBlank() ? URI.create(endpoint.asText()) : null,
2833
region != null && !region.asText().isBlank() ? Region.of(region.asText()) : null,
2934
jsonNode.get("access_key_id").asText(),
30-
jsonNode.get("secret_access_key").asText());
35+
jsonNode.get("secret_access_key").asText(),
36+
attributeNames != null ? Arrays.asList(attributeNames.asText().split("\\s*,\\s*")) : List.of());
3137
}
3238

3339
}

airbyte-integrations/connectors/source-dynamodb/src/main/java/io/airbyte/integrations/source/dynamodb/DynamodbOperations.java

+28-2
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@
1313
import java.time.format.DateTimeParseException;
1414
import java.util.ArrayList;
1515
import java.util.HashMap;
16+
import java.util.HashSet;
1617
import java.util.List;
1718
import java.util.Map;
1819
import java.util.Set;
20+
import java.util.stream.Collectors;
1921
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
2022
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition;
2123
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
@@ -30,7 +32,10 @@ public class DynamodbOperations extends AbstractDatabase implements Closeable {
3032

3133
private ObjectMapper schemaObjectMapper;
3234

35+
private DynamodbConfig dynamodbConfig;
36+
3337
public DynamodbOperations(DynamodbConfig dynamodbConfig) {
38+
this.dynamodbConfig = dynamodbConfig;
3439
this.dynamoDbClient = DynamodbUtils.createDynamoDbClient(dynamodbConfig);
3540
initMappers();
3641
}
@@ -105,12 +110,31 @@ public JsonNode inferSchema(String tableName, int sampleSize) {
105110
public List<JsonNode> scanTable(String tableName, Set<String> attributes, FilterAttribute filterAttribute) {
106111
List<JsonNode> items = new ArrayList<>();
107112

108-
var projectionAttributes = String.join(", ", attributes);
113+
String prefix = "dyndb";
114+
// remove and replace reserved attribute names
115+
Set<String> copyAttributes = new HashSet<>(attributes);
116+
dynamodbConfig.reservedAttributeNames().forEach(copyAttributes::remove);
117+
dynamodbConfig.reservedAttributeNames().stream()
118+
.filter(attributes::contains)
119+
.map(str -> str.replaceAll("[-.]", ""))
120+
.forEach(attr -> copyAttributes.add("#" + prefix + "_" + attr));
121+
122+
Map<String, String> mappingAttributes = dynamodbConfig.reservedAttributeNames().stream()
123+
.filter(attributes::contains)
124+
.collect(Collectors.toUnmodifiableMap(k -> "#" + prefix + "_" + k.replaceAll("[-.]", ""), k -> k));
125+
126+
var projectionAttributes = String.join(", ", copyAttributes);
127+
109128

110129
ScanRequest.Builder scanRequestBuilder = ScanRequest.builder()
111130
.tableName(tableName)
112131
.projectionExpression(projectionAttributes);
113132

133+
if (!mappingAttributes.isEmpty()) {
134+
scanRequestBuilder
135+
.expressionAttributeNames(mappingAttributes);
136+
}
137+
114138
if (filterAttribute != null && filterAttribute.name() != null &&
115139
filterAttribute.value() != null && filterAttribute.type() != null) {
116140

@@ -134,8 +158,10 @@ public List<JsonNode> scanTable(String tableName, Set<String> attributes, Filter
134158
comparator = ">";
135159
}
136160

161+
String filterPlaceholder = dynamodbConfig.reservedAttributeNames().contains(filterName) ?
162+
"#" + prefix + "_" + filterName.replaceAll("[-.]", "") : filterName;
137163
scanRequestBuilder
138-
.filterExpression(filterName + " " + comparator + " :timestamp")
164+
.filterExpression(filterPlaceholder + " " + comparator + " :timestamp")
139165
.expressionAttributeValues(Map.of(":timestamp", attributeValue));
140166

141167
}

airbyte-integrations/connectors/source-dynamodb/src/main/java/io/airbyte/integrations/source/dynamodb/DynamodbUtils.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,9 @@ private static AirbyteStateMessage convertStateMessage(final io.airbyte.protocol
8686

8787
record StreamState(
8888

89-
AirbyteStateMessage.AirbyteStateType airbyteStateType,
89+
AirbyteStateMessage.AirbyteStateType airbyteStateType,
9090

91-
List<AirbyteStateMessage> airbyteStateMessages) {
91+
List<AirbyteStateMessage> airbyteStateMessages) {
9292

9393
}
9494

airbyte-integrations/connectors/source-dynamodb/src/main/resources/spec.json

+7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@
6161
"description": "The corresponding secret to the access key id.",
6262
"airbyte_secret": true,
6363
"examples": ["a012345678910ABCDEFGH/AbCdEfGhEXAMPLEKEY"]
64+
},
65+
"reserved_attribute_names": {
66+
"title": "Reserved attribute names",
67+
"type": "string",
68+
"description": "Comma separated reserved attribute names present in your tables",
69+
"airbyte_secret": true,
70+
"examples": ["name, field_name, field-name"]
6471
}
6572
}
6673
}

airbyte-integrations/connectors/source-dynamodb/src/test-integration/java/io/airbyte/integrations/source/dynamodb/DynamodbDataFactory.java

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public static JsonNode createJsonConfig(DynamodbContainer dynamodbContainer) {
8080
.put("region", dynamodbContainer.getRegion())
8181
.put("access_key_id", dynamodbContainer.getAccessKey())
8282
.put("secret_access_key", dynamodbContainer.getSecretKey())
83+
.put("reserved_attribute_names", "name, field.name, field-name")
8384
.build());
8485
}
8586

airbyte-integrations/connectors/source-dynamodb/src/test-integration/java/io/airbyte/integrations/source/dynamodb/DynamodbOperationsTest.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -153,29 +153,29 @@ void testScanTable() throws JsonProcessingException, JSONException {
153153
PutItemRequest putItemRequest1 = DynamodbDataFactory.putItemRequest(tableName, Map.of(
154154
"attr_1", AttributeValue.builder().s("str_4").build(),
155155
"attr_2", AttributeValue.builder().s("str_5").build(),
156-
"attr_3", AttributeValue.builder().s("2017-12-21T17:42:34Z").build(),
156+
"name", AttributeValue.builder().s("2017-12-21T17:42:34Z").build(),
157157
"attr_4", AttributeValue.builder().ns("12.5", "74.5").build()));
158158

159159
dynamoDbClient.putItem(putItemRequest1);
160160

161161
PutItemRequest putItemRequest2 = DynamodbDataFactory.putItemRequest(tableName, Map.of(
162162
"attr_1", AttributeValue.builder().s("str_6").build(),
163163
"attr_2", AttributeValue.builder().s("str_7").build(),
164-
"attr_3", AttributeValue.builder().s("2019-12-21T17:42:34Z").build(),
164+
"name", AttributeValue.builder().s("2019-12-21T17:42:34Z").build(),
165165
"attr_6", AttributeValue.builder().ss("str_1", "str_2").build()));
166166

167167
dynamoDbClient.putItem(putItemRequest2);
168168

169-
var response = dynamodbOperations.scanTable(tableName, Set.of("attr_1", "attr_2", "attr_3"),
170-
new DynamodbOperations.FilterAttribute("attr_3", "2018-12-21T17:42:34Z",
169+
var response = dynamodbOperations.scanTable(tableName, Set.of("attr_1", "attr_2", "name"),
170+
new DynamodbOperations.FilterAttribute("name", "2018-12-21T17:42:34Z",
171171
DynamodbOperations.FilterAttribute.FilterType.S));
172172

173173
assertThat(response)
174174
.hasSize(1);
175175

176176
JSONAssert.assertEquals(objectMapper.writeValueAsString(response.get(0)), """
177177
{
178-
"attr_3": "2019-12-21T17:42:34Z",
178+
"name": "2019-12-21T17:42:34Z",
179179
"attr_2": "str_7",
180180
"attr_1": "str_6"
181181
}

docs/integrations/sources/dynamodb.md

+12-9
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ This guide describes in details how you can configure the connector to connect w
5151

5252
### Сonfiguration Parameters
5353

54-
* endpoint: aws endpoint of the dynamodb instance
55-
* region: the region code of the dynamodb instance
56-
* access_key_id: the access key for the IAM user with the required permissions
57-
* secret_access_key: the secret key for the IAM user with the required permissions
58-
54+
* **_endpoint_**: aws endpoint of the dynamodb instance
55+
* **_region_**: the region code of the dynamodb instance
56+
* **_access_key_id_**: the access key for the IAM user with the required permissions
57+
* **_secret_access_key_**: the secret key for the IAM user with the required permissions
58+
* **_reserved_attribute_names_**: comma separated list of attribute names present in the replication tables which contain reserved words or special characters. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ExpressionAttributeNames.html
5959

6060
## Changelog
6161

62-
| Version | Date | Pull Request | Subject |
63-
|:--------|:-----------|:-------------|:----------------|
64-
| 0.1.1 | 02-09-2023 | https://github.com/airbytehq/airbyte/pull/22682 | Fix build |
65-
| 0.1.0 | 11-14-2022 | https://github.com/airbytehq/airbyte/pull/18750 | Initial version |
62+
63+
| Version | Date | Pull Request | Subject |
64+
|:--------|:-----------|:------------------------------------------------|:---------------------------------------------------------------------|
65+
| 0.1.2 | 01-19-2023 | https://github.com/airbytehq/airbyte/pull/20172 | Fix reserved words in projection expression & make them configurable |
66+
| 0.1.1 | 02-09-2023 | https://github.com/airbytehq/airbyte/pull/22682 | Fix build |
67+
| 0.1.0 | 11-14-2022 | https://github.com/airbytehq/airbyte/pull/18750 | Initial version |
68+

0 commit comments

Comments
 (0)