Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,14 @@ private Response handleScan(JsonNode request, String region) {
? request.get("ExpressionAttributeNames") : null;
JsonNode exprAttrValues = request.has("ExpressionAttributeValues")
? request.get("ExpressionAttributeValues") : null;
JsonNode scanFilter = request.has("ScanFilter")
? request.get("ScanFilter") : null;
Integer limit = request.has("Limit") ? request.get("Limit").asInt() : null;
JsonNode exclusiveStartKey = request.has("ExclusiveStartKey")
? request.get("ExclusiveStartKey") : null;

DynamoDbService.ScanResult result = dynamoDbService.scan(
tableName, filterExpr, exprAttrNames, exprAttrValues, limit, exclusiveStartKey, region);
tableName, filterExpr, exprAttrNames, exprAttrValues, scanFilter, limit, exclusiveStartKey, region);

ObjectNode response = objectMapper.createObjectNode();
ArrayNode itemsArray = objectMapper.createArrayNode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,21 +513,14 @@ public QueryResult query(String tableName, JsonNode keyConditions,

public ScanResult scan(String tableName, String filterExpression,
JsonNode expressionAttrNames, JsonNode expressionAttrValues,
Integer limit, String startKey) {
JsonNode scanFilter, Integer limit, JsonNode exclusiveStartKey) {
return scan(tableName, filterExpression, expressionAttrNames, expressionAttrValues,
limit, (JsonNode) null, regionResolver.getDefaultRegion());
scanFilter, limit, exclusiveStartKey, regionResolver.getDefaultRegion());
}

public ScanResult scan(String tableName, String filterExpression,
JsonNode expressionAttrNames, JsonNode expressionAttrValues,
Integer limit, String startKey, String region) {
return scan(tableName, filterExpression, expressionAttrNames, expressionAttrValues,
limit, (JsonNode) null, region);
}

public ScanResult scan(String tableName, String filterExpression,
JsonNode expressionAttrNames, JsonNode expressionAttrValues,
Integer limit, JsonNode exclusiveStartKey, String region) {
JsonNode scanFilter, Integer limit, JsonNode exclusiveStartKey, String region) {
String storageKey = regionKey(region, tableName);
TableDefinition table = tableStore.get(storageKey)
.orElseThrow(() -> resourceNotFoundException(tableName));
Expand All @@ -551,10 +544,14 @@ public ScanResult scan(String tableName, String filterExpression,
if (isExpired(item, table)) {
continue;
}
if (filterExpression == null
|| matchesFilterExpression(item, filterExpression, expressionAttrNames, expressionAttrValues)) {
results.add(item);
if (filterExpression != null
&& !matchesFilterExpression(item, filterExpression, expressionAttrNames, expressionAttrValues)) {
continue;
}
if (scanFilter != null && !matchesScanFilter(item, scanFilter)) {
continue;
}
results.add(item);
}

JsonNode lastEvaluatedKey = null;
Expand All @@ -567,6 +564,20 @@ public ScanResult scan(String tableName, String filterExpression,
return new ScanResult(results, totalScanned, lastEvaluatedKey);
}

private boolean matchesScanFilter(JsonNode item, JsonNode scanFilter) {
Iterator<Map.Entry<String, JsonNode>> fields = scanFilter.fields();
while (fields.hasNext()) {
var entry = fields.next();
String attrName = entry.getKey();
JsonNode condition = entry.getValue();
JsonNode attrValue = item.get(attrName);
if (!matchesKeyCondition(attrValue, condition)) {
return false;
}
}
return true;
}

// --- Batch Operations ---

public record BatchWriteResult(Map<String, List<JsonNode>> unprocessedItems) {}
Expand Down Expand Up @@ -1384,6 +1395,7 @@ private String extractComparisonValue(JsonNode condition) {
return null;
}

// NE, CONTAINS, NOT_CONTAINS, IN, NULL, NOT_NULL not yet supported
private boolean matchesKeyCondition(JsonNode attrValue, JsonNode condition) {
if (condition == null) return true;
String op = condition.has("ComparisonOperator") ? condition.get("ComparisonOperator").asText() : "EQ";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,56 @@ void scan() {

@Test
@Order(16)
void scanWithScanFilter() {
given()
.header("X-Amz-Target", "DynamoDB_20120810.Scan")
.contentType(DYNAMODB_CONTENT_TYPE)
.body("""
{
"TableName": "TestTable",
"ScanFilter": {
"name": {
"AttributeValueList": [{"S": "Alice"}],
"ComparisonOperator": "EQ"
}
}
}
""")
.when()
.post("/")
.then()
.statusCode(200)
.body("Count", equalTo(1))
.body("Items[0].name.S", equalTo("Alice"));
}

@Test
@Order(17)
void scanWithScanFilterGE() {
given()
.header("X-Amz-Target", "DynamoDB_20120810.Scan")
.contentType(DYNAMODB_CONTENT_TYPE)
.body("""
{
"TableName": "TestTable",
"ScanFilter": {
"age": {
"AttributeValueList": [{"N": "30"}],
"ComparisonOperator": "GE"
}
}
}
""")
.when()
.post("/")
.then()
.statusCode(200)
.body("Count", equalTo(1))
.body("Items[0].name.S", equalTo("Alice"));
}

@Test
@Order(18)
void deleteItem() {
given()
.header("X-Amz-Target", "DynamoDB_20120810.DeleteItem")
Expand Down Expand Up @@ -474,7 +524,7 @@ void deleteItem() {
}

@Test
@Order(17)
@Order(19)
void deleteTable() {
given()
.header("X-Amz-Target", "DynamoDB_20120810.DeleteTable")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,18 +333,61 @@ void scan() {
service.putItem("Users", item("userId", "u2", "name", "Bob"));
service.putItem("Users", item("userId", "u3", "name", "Charlie"));

DynamoDbService.ScanResult result = service.scan("Users", null, null, null, null, null);
DynamoDbService.ScanResult result = service.scan("Users", null, null, null, null, null, null);
assertEquals(3, result.items().size());
}

@Test
void scanWithScanFilter() {
createUsersTable();
service.putItem("Users", item("userId", "u1", "name", "Alice"));
service.putItem("Users", item("userId", "u2", "name", "Bob"));
service.putItem("Users", item("userId", "u3", "name", "Charlie"));

ObjectNode scanFilter = mapper.createObjectNode();
ObjectNode condition = mapper.createObjectNode();
condition.put("ComparisonOperator", "EQ");
var attrList = mapper.createArrayNode();
ObjectNode val = mapper.createObjectNode();
val.put("S", "Alice");
attrList.add(val);
condition.set("AttributeValueList", attrList);
scanFilter.set("name", condition);

DynamoDbService.ScanResult result = service.scan("Users", null, null, null, scanFilter, null, null);
assertEquals(1, result.items().size());
assertEquals("Alice", result.items().get(0).get("name").get("S").asText());
}

@Test
void scanWithScanFilterGE() {
createUsersTable();
service.putItem("Users", item("userId", "u1", "name", "Alice"));
service.putItem("Users", item("userId", "u2", "name", "Bob"));
service.putItem("Users", item("userId", "u3", "name", "Charlie"));

ObjectNode scanFilter = mapper.createObjectNode();
ObjectNode condition = mapper.createObjectNode();
condition.put("ComparisonOperator", "GE");
var attrList = mapper.createArrayNode();
ObjectNode val = mapper.createObjectNode();
val.put("S", "Bob");
attrList.add(val);
condition.set("AttributeValueList", attrList);
scanFilter.set("name", condition);

DynamoDbService.ScanResult result = service.scan("Users", null, null, null, scanFilter, null, null);
assertEquals(2, result.items().size());
}

@Test
void scanWithLimit() {
createUsersTable();
service.putItem("Users", item("userId", "u1"));
service.putItem("Users", item("userId", "u2"));
service.putItem("Users", item("userId", "u3"));

DynamoDbService.ScanResult result = service.scan("Users", null, null, null, 2, null);
DynamoDbService.ScanResult result = service.scan("Users", null, null, null, null, 2, null);
assertEquals(2, result.items().size());
}

Expand All @@ -354,7 +397,7 @@ void operationsOnNonExistentTableThrow() {
assertThrows(AwsException.class, () -> service.getItem("NoTable", item("id", "1")));
assertThrows(AwsException.class, () -> service.deleteItem("NoTable", item("id", "1")));
assertThrows(AwsException.class, () -> service.query("NoTable", null, null, null, null, null));
assertThrows(AwsException.class, () -> service.scan("NoTable", null, null, null, null, null));
assertThrows(AwsException.class, () -> service.scan("NoTable", null, null, null, null, null, null));
}

@Test
Expand Down Expand Up @@ -531,7 +574,7 @@ void scanWithBoolFilterExpression() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":d", boolAttributeValue(true));

DynamoDbService.ScanResult result = service.scan("Users", "deleted <> :d", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "deleted <> :d", null, exprValues, null, null, null);
assertEquals(2, result.items().size());
}

Expand All @@ -553,7 +596,7 @@ void scanContainsOnListAttribute() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":v", attributeValue("S", "a"));

DynamoDbService.ScanResult result = service.scan("Users", "contains(tags, :v)", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "contains(tags, :v)", null, exprValues, null, null, null);
assertEquals(2, result.items().size());
}

Expand All @@ -571,7 +614,7 @@ void scanContainsOnStringSetAttribute() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":r", attributeValue("S", "admin"));

DynamoDbService.ScanResult result = service.scan("Users", "contains(roles, :r)", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "contains(roles, :r)", null, exprValues, null, null, null);
assertEquals(1, result.items().size());
}

Expand All @@ -596,10 +639,10 @@ void scanAttributeExistsOnNestedMapPath() {
ObjectNode exprNames = mapper.createObjectNode();
exprNames.put("#n", "name");

DynamoDbService.ScanResult result = service.scan("Users", "attribute_exists(info.#n)", exprNames, null, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "attribute_exists(info.#n)", exprNames, null, null, null, null);
assertEquals(2, result.items().size());

DynamoDbService.ScanResult result2 = service.scan("Users", "attribute_not_exists(info.#n)", exprNames, null, null, null);
DynamoDbService.ScanResult result2 = service.scan("Users", "attribute_not_exists(info.#n)", exprNames, null, null, null, null);
assertEquals(1, result2.items().size());
}

Expand Down Expand Up @@ -672,7 +715,7 @@ void scanContainsOnNumberSetWithNumericNormalization() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":v", attributeValue("N", "1.0"));

DynamoDbService.ScanResult result = service.scan("Users", "contains(scores, :v)", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "contains(scores, :v)", null, exprValues, null, null, null);
assertEquals(1, result.items().size(), "contains() on NS should match 1.0 == 1 numerically");
}

Expand All @@ -690,7 +733,7 @@ void scanContainsOnBinarySet() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":v", attributeValue("B", "AQID"));

DynamoDbService.ScanResult result = service.scan("Users", "contains(bins, :v)", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "contains(bins, :v)", null, exprValues, null, null, null);
assertEquals(1, result.items().size());
}

Expand Down Expand Up @@ -718,7 +761,7 @@ void scanContainsOnListWithNumericElements() {
ObjectNode exprValues = mapper.createObjectNode();
exprValues.set(":v", attributeValue("N", "10.0"));

DynamoDbService.ScanResult result = service.scan("Users", "contains(values, :v)", null, exprValues, null, null);
DynamoDbService.ScanResult result = service.scan("Users", "contains(values, :v)", null, exprValues, null, null, null);
assertEquals(1, result.items().size(), "contains() on List with N elements should use type-aware numeric comparison");
}
}