Skip to content

Commit 9614e0c

Browse files
authored
Merge pull request #36 from oracle/bugfix/KVSTORE-1923
[kvstore-1923] Handle table DDL mismatch
2 parents a12a59b + 0f379ee commit 9614e0c

File tree

12 files changed

+602
-52
lines changed

12 files changed

+602
-52
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.
33

44
The format is based on [Keep a Changelog](http://keepachangelog.com/).
55

6+
## [1.7.0] [unreleased]
7+
### Added
8+
- Added the checks to verify entity definition matches with corresponding
9+
table in the database during table creation.
10+
11+
### Changed
12+
613
## [1.6.0]
714
### Added
815
- Added support for composite primary keys.

config/checkstyle-suppressions.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0"?>
2+
3+
<!DOCTYPE suppressions PUBLIC
4+
"-//Checkstyle//DTD SuppressionFilter Configuration 1.0//EN"
5+
"https://checkstyle.org/dtds/suppressions_1_0.dtd">
6+
7+
<suppressions>
8+
<suppress checks="^[a-z][a-zA-Z0-9]*"
9+
files="TestTableCreation.java"/>
10+
</suppressions>

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@
298298
<phase>validate</phase>
299299
<configuration>
300300
<configLocation>${project.basedir}/config/checkstyle.xml</configLocation>
301+
<suppressionsLocation>${project.basedir}/config/checkstyle-suppressions.xml</suppressionsLocation>
301302
<encoding>UTF-8</encoding>
302303
<consoleOutput>true</consoleOutput>
303304
<failsOnError>true</failsOnError>

src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplate.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ public String getTableName(Class<?> domainClass) {
9393
@Override
9494
public boolean createTableIfNotExists(
9595
NosqlEntityInformation<?, ?> entityInformation) {
96-
return doCreateTableIfNotExists(entityInformation);
96+
boolean isTableExist = doCheckExistingTable(entityInformation);
97+
// if table does not exist create
98+
return (!isTableExist) ? doCreateTable(entityInformation) : true;
9799
}
98100

99101
@SuppressWarnings("unchecked")

src/main/java/com/oracle/nosql/spring/data/core/NosqlTemplateBase.java

Lines changed: 200 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,22 @@
66
*/
77
package com.oracle.nosql.spring.data.core;
88

9+
import java.util.ArrayList;
910
import java.util.HashMap;
1011
import java.util.LinkedHashMap;
12+
import java.util.List;
1113
import java.util.Map;
14+
import java.util.stream.Collectors;
1215

1316
import oracle.nosql.driver.NoSQLException;
1417
import oracle.nosql.driver.NoSQLHandle;
1518
import oracle.nosql.driver.TableNotFoundException;
19+
import oracle.nosql.driver.TimeToLive;
1620
import oracle.nosql.driver.ops.DeleteRequest;
1721
import oracle.nosql.driver.ops.DeleteResult;
1822
import oracle.nosql.driver.ops.GetRequest;
1923
import oracle.nosql.driver.ops.GetResult;
24+
import oracle.nosql.driver.ops.GetTableRequest;
2025
import oracle.nosql.driver.ops.PrepareRequest;
2126
import oracle.nosql.driver.ops.PrepareResult;
2227
import oracle.nosql.driver.ops.PreparedStatement;
@@ -26,7 +31,10 @@
2631
import oracle.nosql.driver.ops.TableRequest;
2732
import oracle.nosql.driver.ops.TableResult;
2833
import oracle.nosql.driver.util.LruCache;
34+
import oracle.nosql.driver.values.ArrayValue;
2935
import oracle.nosql.driver.values.FieldValue;
36+
import oracle.nosql.driver.values.JsonOptions;
37+
import oracle.nosql.driver.values.JsonUtils;
3038
import oracle.nosql.driver.values.MapValue;
3139

3240
import com.oracle.nosql.spring.data.NosqlDbFactory;
@@ -112,9 +120,7 @@ protected TableResult doTableRequest(NosqlEntityInformation<?, ?> entityInformat
112120
return tableRes;
113121
}
114122

115-
protected boolean doCreateTableIfNotExists(
116-
NosqlEntityInformation<?, ?> entityInformation) {
117-
123+
protected String getCreateTableDDL(NosqlEntityInformation<?, ?> entityInformation) {
118124
String tableName = entityInformation.getTableName();
119125
String sql;
120126

@@ -168,8 +174,12 @@ protected boolean doCreateTableIfNotExists(
168174
entityInformation.getTtl().toString()));
169175
}
170176
sql = tableBuilder.toString();
177+
return sql;
178+
}
171179

172-
TableRequest tableReq = new TableRequest().setStatement(sql)
180+
protected boolean doCreateTable(NosqlEntityInformation<?, ?> entityInformation) {
181+
String ddl = getCreateTableDDL(entityInformation);
182+
TableRequest tableReq = new TableRequest().setStatement(ddl)
173183
.setTableLimits(entityInformation.getTableLimits(nosqlDbFactory));
174184

175185
TableResult tableRes = doTableRequest(entityInformation, tableReq);
@@ -178,6 +188,179 @@ protected boolean doCreateTableIfNotExists(
178188
return tableState == TableResult.State.ACTIVE;
179189
}
180190

191+
protected boolean doCheckExistingTable(NosqlEntityInformation<?, ?> entityInformation) {
192+
final String colField = "fields";
193+
final String colNameField = "name";
194+
final String colTypeField = "type";
195+
final String shardField = "shardKey";
196+
final String primaryField = "primaryKey";
197+
final String ttlField = "ttl";
198+
final String identityField = "identity";
199+
200+
List<String> errors = new ArrayList<>();
201+
202+
TableResult tableResult = null;
203+
String jsonSchema = null;
204+
try {
205+
tableResult = doGetTable(entityInformation);
206+
207+
// table does not exist return false
208+
if (tableResult == null) {
209+
return false;
210+
}
211+
212+
/* If table already exist in the database compare and throw error if
213+
mismatch*/
214+
jsonSchema = tableResult.getSchema();
215+
MapValue tableSchema = JsonUtils.createValueFromJson(jsonSchema,
216+
new JsonOptions().setMaintainInsertionOrder(true)).
217+
asMap();
218+
219+
ArrayValue tableColumns = tableSchema.get(colField).asArray();
220+
ArrayValue tableShardKeys =
221+
tableSchema.get(shardField).asArray();
222+
ArrayValue tablePrimaryKeys =
223+
tableSchema.get(primaryField).asArray();
224+
225+
Map<String, String> tableShardMap = new LinkedHashMap<>();
226+
Map<String, String> tableNonShardMap = new LinkedHashMap<>();
227+
Map<String, String> tableOthersMap = new LinkedHashMap<>();
228+
229+
// extract table details into maps
230+
for (int i = 0; i < tableColumns.size(); i++) {
231+
MapValue column = tableColumns.get(i).asMap();
232+
String colName =
233+
column.getString(colNameField).toLowerCase();
234+
String columnType =
235+
column.getString(colTypeField).toLowerCase();
236+
237+
if (i < tableShardKeys.size()) {
238+
tableShardMap.put(colName, columnType);
239+
} else if (i < tablePrimaryKeys.size()) {
240+
tableNonShardMap.put(colName, columnType);
241+
} else {
242+
tableOthersMap.put(colName, columnType);
243+
}
244+
}
245+
246+
// extract entity details into maps
247+
Map<String, FieldValue.Type> shardKeys = entityInformation.
248+
getShardKeys();
249+
Map<String, FieldValue.Type> nonShardKeys = entityInformation.
250+
getNonShardKeys();
251+
252+
Map<String, String> entityShardMap = new LinkedHashMap<>();
253+
shardKeys.forEach((k, v) -> entityShardMap.put(
254+
k.toLowerCase(), v.name().toLowerCase()));
255+
256+
Map<String, String> entityNonShardMap = new LinkedHashMap<>();
257+
nonShardKeys.forEach((k, v) -> entityNonShardMap.put(
258+
k.toLowerCase(), v.name().toLowerCase()));
259+
260+
Map<String, String> entityOthersMap = new LinkedHashMap<>();
261+
entityOthersMap.put(JSON_COLUMN.toLowerCase(), "json");
262+
263+
// convert maps to String. String format is {k1 v1, k2 v2 ...}
264+
String tableShards = "{" + tableShardMap.entrySet().stream()
265+
.map(e -> e.getKey() + " " + e.getValue()).
266+
collect(Collectors.joining(",")) + "}";
267+
String tableNonShards = "{" + tableNonShardMap.entrySet()
268+
.stream().map(e -> e.getKey() + " " + e.getValue()).
269+
collect(Collectors.joining(",")) + "}";
270+
271+
String entityShards = "{" + entityShardMap.entrySet().stream()
272+
.map(e -> e.getKey() + " " + e.getValue()).
273+
collect(Collectors.joining(",")) + "}";
274+
String entityNonShards = "{" + entityNonShardMap.entrySet()
275+
.stream().map(e -> e.getKey() + " " + e.getValue()).
276+
collect(Collectors.joining(",")) + "}";
277+
278+
String msg;
279+
// check shard keys and types match
280+
if (!tableShards.equals(entityShards)) {
281+
msg = String.format("Shard primary keys mismatch: " +
282+
"table=%s, entity=%s.", tableShards, entityShards);
283+
errors.add(msg);
284+
}
285+
286+
// check non-shard keys and types match
287+
if (!tableNonShards.equals(entityNonShards)) {
288+
msg = String.format("Non-shard primary keys mismatch: " +
289+
"table=%s, entity=%s.", tableNonShards,
290+
entityNonShards);
291+
errors.add(msg);
292+
}
293+
294+
// check kv_json_ column exist and it's type is JSON
295+
if (!tableOthersMap.containsKey(JSON_COLUMN.toLowerCase())) {
296+
msg = String.format("'%s' column does not exist in the table",
297+
JSON_COLUMN);
298+
errors.add(msg);
299+
} else if (!tableOthersMap.get(JSON_COLUMN.toLowerCase()).
300+
equalsIgnoreCase("json")) {
301+
msg = String.format("'%s' column type is not JSON in the " +
302+
"table", JSON_COLUMN);
303+
errors.add(msg);
304+
}
305+
306+
// check identity same
307+
FieldValue identity = tableSchema.get(identityField);
308+
if (identity != null && !entityInformation.isAutoGeneratedId()) {
309+
errors.add("Identity information mismatch.");
310+
311+
} else if (identity == null && entityInformation.isAutoGeneratedId() &&
312+
entityInformation.getIdNosqlType() != FieldValue.Type.STRING) {
313+
errors.add("Identity information mismatch.");
314+
}
315+
316+
// TTL warning
317+
FieldValue ttlValue = tableSchema.get(ttlField);
318+
TimeToLive ttl = entityInformation.getTtl();
319+
// TTL is present in database but not in the entity
320+
if (ttlValue != null && ttl != null &&
321+
!ttl.toString().equalsIgnoreCase(ttlValue.getString())) {
322+
LOG.warn("TTL of the table in database is different from " +
323+
"the TTL of the entity " +
324+
entityInformation.getJavaType().getName());
325+
} else if (ttlValue == null && ttl != null && ttl.getValue() != 0) {
326+
// TTL is present in entity but not in the database
327+
LOG.warn("TTL of the table in database is different from " +
328+
"the TTL of the entity " +
329+
entityInformation.getJavaType().getName());
330+
}
331+
} catch (NullPointerException | ClassCastException ex) {
332+
// something is wrong in parsing json schema
333+
String msg = String.format("Could not validate schema of the " +
334+
"table %s in the database against entity %s : " +
335+
"%s",
336+
entityInformation.getTableName(),
337+
entityInformation.getJavaType().getName(),
338+
ex.getMessage());
339+
340+
if (LOG.isDebugEnabled()) {
341+
LOG.debug("JSON Schema of the table is " + jsonSchema);
342+
}
343+
LOG.warn(msg);
344+
}
345+
346+
if (!errors.isEmpty()) {
347+
StringBuilder sb = new StringBuilder();
348+
sb.append("The following mismatch have been found between the " +
349+
"entity class ");
350+
sb.append(entityInformation.getJavaType().getName());
351+
sb.append(" definition and the existing table ");
352+
sb.append(entityInformation.getTableName());
353+
sb.append(" in the database:\n");
354+
errors.forEach(err -> sb.append(err).append("\n"));
355+
sb.append("To fix this errors, either make the entity class to" +
356+
" match the table definition or use NosqlTable.name" +
357+
" annotation to use a different table.");
358+
throw new IllegalArgumentException(sb.toString());
359+
}
360+
// no mismatch between table and entity return true
361+
return true;
362+
}
363+
181364
protected DeleteResult doDelete(
182365
NosqlEntityInformation<?, ?> entityInformation,
183366
MapValue primaryKey) {
@@ -229,16 +412,7 @@ protected PutResult doPut(NosqlEntityInformation<?, ?> entityInformation,
229412

230413
PutResult putRes;
231414
try {
232-
try {
233-
putRes = nosqlClient.put(putReq);
234-
} catch (TableNotFoundException tnfe) {
235-
if (entityInformation.isAutoCreateTable()) {
236-
doCreateTableIfNotExists(entityInformation);
237-
putRes = nosqlClient.put(putReq);
238-
} else {
239-
throw tnfe;
240-
}
241-
}
415+
putRes = nosqlClient.put(putReq);
242416
} catch (NoSQLException nse) {
243417
LOG.error("Put: table: {} key: {}", putReq.getTableName(),
244418
row.get(entityInformation.getIdColumnName()));
@@ -378,6 +552,18 @@ protected <T> Iterable<MapValue> doExecuteMapValueQuery(NosqlQuery query,
378552
return doQuery(qReq);
379553
}
380554

555+
protected TableResult doGetTable(NosqlEntityInformation<?, ?> entityInformation) {
556+
try {
557+
GetTableRequest request = new GetTableRequest();
558+
request.setTableName(entityInformation.getTableName());
559+
return nosqlClient.getTable(request);
560+
} catch (TableNotFoundException tne) {
561+
return null;
562+
} catch (NoSQLException nse) {
563+
throw MappingNosqlConverter.convert(nse);
564+
}
565+
}
566+
381567
private PreparedStatement getPreparedStatement(
382568
NosqlEntityInformation<?, ?> entityInformation, String query) {
383569
PreparedStatement preparedStatement;

src/main/java/com/oracle/nosql/spring/data/core/ReactiveNosqlTemplate.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,10 @@ public String getTableName(Class<?> domainClass) {
7878
public Mono<Boolean> createTableIfNotExists(
7979
NosqlEntityInformation<?, ?> entityInformation) {
8080
Assert.notNull(entityInformation, "Entity information should not be null");
81-
82-
return Mono.just(doCreateTableIfNotExists(entityInformation));
81+
boolean isTableExist = doCheckExistingTable(entityInformation);
82+
// if table does not exist create
83+
return (!isTableExist) ? Mono.just(doCreateTable(entityInformation)) :
84+
Mono.just(true);
8385
}
8486

8587
/**

src/main/java/com/oracle/nosql/spring/data/repository/support/NosqlEntityInformation.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,8 @@ public Map<String, FieldValue.Type> getNonShardKeys() {
463463
}
464464

465465
private static class ProcessPrimaryKeys {
466-
private Map<String, FieldValue.Type> shardKeys;
467-
private Map<String, FieldValue.Type> nonShardKeys;
466+
private final Map<String, FieldValue.Type> shardKeys;
467+
private final Map<String, FieldValue.Type> nonShardKeys;
468468

469469
public ProcessPrimaryKeys(Field idField) {
470470
shardKeys = new LinkedHashMap<>();
@@ -477,8 +477,8 @@ private void process(Field idField) {
477477

478478
if (isCompositeKeyType(idField.getType())) {
479479
//composite key
480-
Map<Integer, SortedSet<String>> shardMap = new TreeMap<>();
481-
Map<Integer, SortedSet<String>> nonShardMap = new TreeMap<>();
480+
TreeMap<Integer, SortedSet<String>> shardMap = new TreeMap<>();
481+
TreeMap<Integer, SortedSet<String>> nonShardMap = new TreeMap<>();
482482

483483
for (Field primaryKey : idField.getType().getDeclaredFields()) {
484484
if (!primaryKey.isAnnotationPresent(Transient.class) &&
@@ -578,10 +578,8 @@ private void process(Field idField) {
578578
}
579579

580580
if (!nonShardMap.isEmpty()) {
581-
int shardMaxOrder =
582-
(int) ((TreeMap<?, ?>) shardMap).lastKey();
583-
int nonShardMinOrder =
584-
(int) ((TreeMap<?, ?>) nonShardMap).firstKey();
581+
int shardMaxOrder = shardMap.lastKey();
582+
int nonShardMinOrder = nonShardMap.firstKey();
585583

586584
if (shardMaxOrder != -1 && nonShardMinOrder <= shardMaxOrder) {
587585
throw new IllegalArgumentException("Order of non " +

src/test/java/com/oracle/nosql/spring/data/test/TestTTL.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public void setup() {
4747
template.dropTableIfExists(EntityWith10DaysTTL.class.getSimpleName());
4848
template.dropTableIfExists(EntityWithDefaultTTL.class.getSimpleName());
4949
template.dropTableIfExists(EntityWithNegativeTTL.class.getSimpleName());
50+
51+
template.createTableIfNotExists(template.
52+
getNosqlEntityInformation(EntityWith10DaysTTL.class));
53+
template.createTableIfNotExists(template.
54+
getNosqlEntityInformation(EntityWithDefaultTTL.class));
5055
}
5156

5257
@After

src/test/java/com/oracle/nosql/spring/data/test/composite/MachineApp.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ public static void staticSetup() throws ClassNotFoundException {
5353
@Before
5454
public void setup() {
5555
template.dropTableIfExists(Machine.class.getSimpleName());
56-
56+
template.createTableIfNotExists(template.
57+
getNosqlEntityInformation(Machine.class));
5758
machineCache = new HashMap<>();
5859
List<IpAddress> routeAddress = new ArrayList<>();
5960
routeAddress.add(new IpAddress("127.0.0.1"));

src/test/java/com/oracle/nosql/spring/data/test/composite/MachineAppWithoutAnnotation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ public static void staticSetup() throws ClassNotFoundException {
5151
@Before
5252
public void setup() {
5353
template.dropTableIfExists(MachineWithoutAnnotation.class.getSimpleName());
54+
template.createTableIfNotExists(template.
55+
getNosqlEntityInformation(MachineWithoutAnnotation.class));
5456
}
5557

5658
@After

0 commit comments

Comments
 (0)