Skip to content

Commit fae31ed

Browse files
Backport to branch(3.11) : Add validation for primary key columns in Cosmos DB adapter (#2326)
Co-authored-by: Toshihiro Suzuki <[email protected]>
1 parent 2090a32 commit fae31ed

File tree

4 files changed

+559
-17
lines changed

4 files changed

+559
-17
lines changed

core/src/main/java/com/scalar/db/storage/cosmos/ConcatenationVisitor.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
import javax.annotation.concurrent.NotThreadSafe;
1515

1616
/**
17-
* A visitor class to make a concatenated key string for the partition key
17+
* A visitor class to make a concatenated key string for the partition key. This uses a colon as a
18+
* key separator, so the text column value should not contain colons.
1819
*
1920
* @author Yuji Ito
2021
*/
@@ -27,7 +28,6 @@ public ConcatenationVisitor() {
2728
}
2829

2930
public String build() {
30-
// TODO What if the string or blob value includes `:`?
3131
return String.join(":", values);
3232
}
3333

core/src/main/java/com/scalar/db/storage/cosmos/CosmosOperationChecker.java

+95
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,130 @@
22

33
import com.scalar.db.api.ConditionalExpression;
44
import com.scalar.db.api.Delete;
5+
import com.scalar.db.api.Get;
56
import com.scalar.db.api.Mutation;
7+
import com.scalar.db.api.Operation;
68
import com.scalar.db.api.Put;
9+
import com.scalar.db.api.Scan;
710
import com.scalar.db.api.TableMetadata;
811
import com.scalar.db.common.TableMetadataManager;
912
import com.scalar.db.common.checker.OperationChecker;
1013
import com.scalar.db.config.DatabaseConfig;
1114
import com.scalar.db.exception.storage.ExecutionException;
15+
import com.scalar.db.io.BigIntColumn;
16+
import com.scalar.db.io.BlobColumn;
17+
import com.scalar.db.io.BooleanColumn;
18+
import com.scalar.db.io.ColumnVisitor;
1219
import com.scalar.db.io.DataType;
20+
import com.scalar.db.io.DoubleColumn;
21+
import com.scalar.db.io.FloatColumn;
22+
import com.scalar.db.io.IntColumn;
23+
import com.scalar.db.io.TextColumn;
1324

1425
public class CosmosOperationChecker extends OperationChecker {
26+
27+
private static final char[] ILLEGAL_CHARACTERS_IN_PRIMARY_KEY = {
28+
// Colons are not allowed in primary-key columns due to the `ConcatenationVisitor` limitation.
29+
':',
30+
31+
// The following characters are not allowed in primary-key columns because they are restricted
32+
// and cannot be used in the `Id` property of a Cosmos DB document. For more information, see:
33+
// https://learn.microsoft.com/en-us/dotnet/api/microsoft.azure.cosmos.databaseproperties.id?view=azure-dotnet#remarks
34+
'/',
35+
'\\',
36+
'#',
37+
'?'
38+
};
39+
40+
private static final ColumnVisitor PRIMARY_KEY_COLUMN_CHECKER =
41+
new ColumnVisitor() {
42+
@Override
43+
public void visit(BooleanColumn column) {}
44+
45+
@Override
46+
public void visit(IntColumn column) {}
47+
48+
@Override
49+
public void visit(BigIntColumn column) {}
50+
51+
@Override
52+
public void visit(FloatColumn column) {}
53+
54+
@Override
55+
public void visit(DoubleColumn column) {}
56+
57+
@Override
58+
public void visit(TextColumn column) {
59+
String value = column.getTextValue();
60+
assert value != null;
61+
62+
for (char illegalCharacter : ILLEGAL_CHARACTERS_IN_PRIMARY_KEY) {
63+
if (value.indexOf(illegalCharacter) != -1) {
64+
throw new IllegalArgumentException(
65+
String.format(
66+
"The value of the column %s in the primary key contains an illegal character. "
67+
+ "Primary-key columns must not contain any of the following characters in Cosmos DB: ':', '/', '\\', '#', '?'. Value: %s",
68+
column.getName(), value));
69+
}
70+
}
71+
}
72+
73+
@Override
74+
public void visit(BlobColumn column) {}
75+
};
76+
1577
public CosmosOperationChecker(
1678
DatabaseConfig databaseConfig, TableMetadataManager metadataManager) {
1779
super(databaseConfig, metadataManager);
1880
}
1981

82+
@Override
83+
public void check(Get get) throws ExecutionException {
84+
super.check(get);
85+
checkPrimaryKey(get);
86+
}
87+
88+
@Override
89+
public void check(Scan scan) throws ExecutionException {
90+
super.check(scan);
91+
checkPrimaryKey(scan);
92+
scan.getStartClusteringKey()
93+
.ifPresent(
94+
c -> c.getColumns().forEach(column -> column.accept(PRIMARY_KEY_COLUMN_CHECKER)));
95+
scan.getEndClusteringKey()
96+
.ifPresent(
97+
c -> c.getColumns().forEach(column -> column.accept(PRIMARY_KEY_COLUMN_CHECKER)));
98+
}
99+
20100
@Override
21101
public void check(Put put) throws ExecutionException {
22102
super.check(put);
103+
checkPrimaryKey(put);
104+
23105
TableMetadata metadata = getTableMetadata(put);
24106
checkCondition(put, metadata);
25107
}
26108

27109
@Override
28110
public void check(Delete delete) throws ExecutionException {
29111
super.check(delete);
112+
checkPrimaryKey(delete);
113+
30114
TableMetadata metadata = getTableMetadata(delete);
31115
checkCondition(delete, metadata);
32116
}
33117

118+
private void checkPrimaryKey(Operation operation) {
119+
operation
120+
.getPartitionKey()
121+
.getColumns()
122+
.forEach(column -> column.accept(PRIMARY_KEY_COLUMN_CHECKER));
123+
operation
124+
.getClusteringKey()
125+
.ifPresent(
126+
c -> c.getColumns().forEach(column -> column.accept(PRIMARY_KEY_COLUMN_CHECKER)));
127+
}
128+
34129
private void checkCondition(Mutation mutation, TableMetadata metadata) {
35130
if (!mutation.getCondition().isPresent()) {
36131
return;

0 commit comments

Comments
 (0)