Skip to content

Commit c91c57e

Browse files
author
Yingjian Wu
committed
Add validaton to prevent creating another new name with a different uuid
1 parent 8317b70 commit c91c57e

File tree

3 files changed

+136
-14
lines changed

3 files changed

+136
-14
lines changed

metacat-functional-tests/src/functionalTest/groovy/com/netflix/metacat/ParentChildRelMetadataServiceSpec.groovy

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ class ParentChildRelMetadataServiceSpec extends Specification{
2929
@Shared
3030
private String database = "testpc"
3131

32+
@Shared
33+
static final String SQL_CREATE_PARENT_CHILD_RELATIONS =
34+
"INSERT INTO parent_child_relation (parent, parent_uuid, child, child_uuid, relation_type) " +
35+
"VALUES (?, ?, ?, ?, ?)"
36+
37+
private void insertNewParentChildRecord(final String pName, final String pUUID,
38+
final String child, final String childUUID, final String type) {
39+
jdbcTemplate.update(SQL_CREATE_PARENT_CHILD_RELATIONS, pName, pUUID, child, childUUID, type)
40+
}
41+
3242
def setupSpec() {
3343
String jdbcUrl = "jdbc:mysql://localhost:3306/metacat"
3444
String username = "metacat_user"
@@ -481,4 +491,36 @@ class ParentChildRelMetadataServiceSpec extends Specification{
481491
assert service.isChildTable(child2)
482492
assert !service.isParentTable(child2)
483493
}
494+
495+
// This could happen in 2 cases:
496+
// 1. Fail to create the table but did not clean up the parent child relationship
497+
// 2: Successfully Drop the table but fail to clean up the parent child relationship
498+
def "simulate a record is not cleaned up and a same parent or child name is created"() {
499+
given:
500+
def parent = QualifiedName.ofTable(catalog, database, "p")
501+
def parentUUID = "parent_uuid"
502+
def child = QualifiedName.ofTable(catalog, database, "c")
503+
def childUUID = "child_uuid"
504+
def type = "clone"
505+
def randomParent = QualifiedName.ofTable(catalog, database, "rp")
506+
def randomParentUUID = "random_parent_uuid"
507+
def randomChild = QualifiedName.ofTable(catalog, database, "rc")
508+
def randomChildUUID = "random_child_uuid"
509+
510+
insertNewParentChildRecord(parent.toString(), parentUUID, child.toString(), childUUID, type)
511+
512+
// Same parent name is created with a different uuid should fail
513+
when:
514+
service.createParentChildRelation(parent, randomParentUUID, randomChild, randomChildUUID, type)
515+
then:
516+
def e = thrown(RuntimeException)
517+
assert e.message.contains("This normally means table prodhive/testpc/p already exists")
518+
519+
// Same childName with a different uuid should fail
520+
when:
521+
service.createParentChildRelation(randomParent, randomParentUUID, child, randomChildUUID, type)
522+
then:
523+
e = thrown(RuntimeException)
524+
assert e.message.contains("Cannot have a child table having more than one parent")
525+
}
484526
}

metacat-main/src/test/groovy/com/netflix/metacat/main/services/impl/TableServiceImplSpec.groovy

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import com.netflix.metacat.common.server.events.MetacatEventBus
3636
import com.netflix.metacat.common.server.events.MetacatUpdateTablePostEvent
3737
import com.netflix.metacat.common.server.events.MetacatUpdateTablePreEvent
3838
import com.netflix.metacat.common.server.model.ChildInfo
39-
import com.netflix.metacat.common.server.model.ParentInfo
4039
import com.netflix.metacat.common.server.properties.Config
4140
import com.netflix.metacat.common.server.spi.MetacatCatalogConfig
4241
import com.netflix.metacat.common.server.usermetadata.DefaultAuthorizationService
@@ -292,6 +291,23 @@ class TableServiceImplSpec extends Specification {
292291
definitionMetadata: outerNode,
293292
serde: new StorageDto(uri: 's3:/clone/clone/c')
294293
)
294+
// mock case where create Table Fail as parent table does not exist
295+
when:
296+
service.create(childTableName, createTableDto)
297+
then:
298+
1 * config.isParentChildCreateEnabled() >> true
299+
1 * ownerValidationService.extractPotentialOwners(_) >> ["cloneClient"]
300+
1 * ownerValidationService.isUserValid(_) >> true
301+
1 * ownerValidationService.extractPotentialOwnerGroups(_) >> ["cloneClientGroup"]
302+
1 * ownerValidationService.isGroupValid(_) >> true
303+
1 * connectorTableServiceProxy.exists(_) >> false
304+
305+
0 * parentChildRelSvc.createParentChildRelation(parentTableName, "p_uuid", childTableName, "child_uuid", "CLONE")
306+
0 * connectorTableServiceProxy.create(_, _) >> {throw new RuntimeException("Fail to create")}
307+
0 * parentChildRelSvc.deleteParentChildRelation(parentTableName, "p_uuid", childTableName, "child_uuid", "CLONE")
308+
def e = thrown(RuntimeException)
309+
assert e.message.contains("does not exist")
310+
295311
// mock case where create Table Fail and revert function is triggerred
296312
when:
297313
service.create(childTableName, createTableDto)
@@ -301,6 +317,7 @@ class TableServiceImplSpec extends Specification {
301317
1 * ownerValidationService.isUserValid(_) >> true
302318
1 * ownerValidationService.extractPotentialOwnerGroups(_) >> ["cloneClientGroup"]
303319
1 * ownerValidationService.isGroupValid(_) >> true
320+
1 * connectorTableServiceProxy.exists(_) >> true
304321

305322
1 * parentChildRelSvc.createParentChildRelation(parentTableName, "p_uuid", childTableName, "child_uuid", "CLONE")
306323
1 * connectorTableServiceProxy.create(_, _) >> {throw new RuntimeException("Fail to create")}
@@ -317,10 +334,11 @@ class TableServiceImplSpec extends Specification {
317334
1 * ownerValidationService.isUserValid(_) >> true
318335
1 * ownerValidationService.extractPotentialOwnerGroups(_) >> ["cloneClientGroup"]
319336
1 * ownerValidationService.isGroupValid(_) >> true
337+
0 * connectorTableServiceProxy.exists(_)
320338

321339
0 * parentChildRelSvc.createParentChildRelation(parentTableName, "p_uuid", childTableName, "child_uuid", "CLONE")
322340
0 * connectorTableServiceProxy.create(_, _)
323-
def e = thrown(RuntimeException)
341+
e = thrown(RuntimeException)
324342
assert e.message.contains("is currently disabled")
325343

326344
// mock successful case
@@ -332,6 +350,7 @@ class TableServiceImplSpec extends Specification {
332350
1 * ownerValidationService.isUserValid(_) >> true
333351
1 * ownerValidationService.extractPotentialOwnerGroups(_) >> ["cloneClientGroup"]
334352
1 * ownerValidationService.isGroupValid(_) >> true
353+
1 * connectorTableServiceProxy.exists(_) >> true
335354

336355
1 * parentChildRelSvc.createParentChildRelation(parentTableName, "p_uuid", childTableName, "child_uuid", "CLONE")
337356
1 * connectorTableServiceProxy.create(_, _)
@@ -417,7 +436,8 @@ class TableServiceImplSpec extends Specification {
417436
1 * config.isParentChildGetEnabled() >> true
418437
1 * config.isParentChildDropEnabled() >> true
419438
1 * parentChildRelSvc.getParents(name) >> {[] as Set}
420-
2 * parentChildRelSvc.getChildren(name) >> {[new ChildInfo("child", "clone", "child_uuid")] as Set}
439+
1 * parentChildRelSvc.isParentTable(name) >> true
440+
1 * parentChildRelSvc.getChildren(name) >> {[new ChildInfo("child", "clone", "child_uuid")] as Set}
421441
1 * config.getNoTableDeleteOnTags() >> []
422442
def e = thrown(RuntimeException)
423443
assert e.message.contains("because it still has")
@@ -429,7 +449,8 @@ class TableServiceImplSpec extends Specification {
429449
1 * config.isParentChildGetEnabled() >> true
430450
1 * config.isParentChildDropEnabled() >> true
431451
1 * parentChildRelSvc.getParents(name)
432-
2 * parentChildRelSvc.getChildren(name)
452+
1 * parentChildRelSvc.isParentTable(name) >> true
453+
1 * parentChildRelSvc.getChildren(name)
433454
1 * config.getNoTableDeleteOnTags() >> []
434455
1 * connectorTableServiceProxy.delete(_) >> {throw new RuntimeException("Fail to drop")}
435456
0 * parentChildRelSvc.drop(_)
@@ -442,9 +463,9 @@ class TableServiceImplSpec extends Specification {
442463
1 * config.isParentChildGetEnabled() >> true
443464
1 * config.isParentChildDropEnabled() >> false
444465
1 * parentChildRelSvc.isChildTable(name) >> true
445-
0 * parentChildRelSvc.isParentTable(name)
466+
1 * parentChildRelSvc.isParentTable(name) >> false
446467
1 * parentChildRelSvc.getParents(name)
447-
1 * parentChildRelSvc.getChildren(name)
468+
0 * parentChildRelSvc.getChildren(name)
448469
1 * config.getNoTableDeleteOnTags() >> []
449470
0 * connectorTableServiceProxy.delete(_)
450471
0 * parentChildRelSvc.drop(_)
@@ -458,9 +479,9 @@ class TableServiceImplSpec extends Specification {
458479
1 * config.isParentChildGetEnabled() >> true
459480
1 * config.isParentChildDropEnabled() >> false
460481
1 * parentChildRelSvc.isChildTable(name) >> false
461-
1 * parentChildRelSvc.isParentTable(name) >> true
482+
2 * parentChildRelSvc.isParentTable(name) >> true
462483
1 * parentChildRelSvc.getParents(name)
463-
1 * parentChildRelSvc.getChildren(name)
484+
0 * parentChildRelSvc.getChildren(name)
464485
1 * config.getNoTableDeleteOnTags() >> []
465486
0 * connectorTableServiceProxy.delete(_)
466487
0 * parentChildRelSvc.drop(_)
@@ -475,9 +496,9 @@ class TableServiceImplSpec extends Specification {
475496
1 * config.isParentChildGetEnabled() >> true
476497
1 * config.isParentChildDropEnabled() >> false
477498
1 * parentChildRelSvc.isChildTable(name) >> false
478-
1 * parentChildRelSvc.isParentTable(name) >> false
499+
2 * parentChildRelSvc.isParentTable(name) >> false
479500
1 * parentChildRelSvc.getParents(name) >> {[] as Set}
480-
2 * parentChildRelSvc.getChildren(name) >> {[] as Set}
501+
1 * parentChildRelSvc.getChildren(name) >> {[] as Set}
481502
1 * config.getNoTableDeleteOnTags() >> []
482503
1 * connectorTableServiceProxy.delete(_)
483504
1 * parentChildRelSvc.drop(_)
@@ -491,9 +512,9 @@ class TableServiceImplSpec extends Specification {
491512
1 * config.isParentChildGetEnabled() >> true
492513
1 * config.isParentChildDropEnabled() >> true
493514
0 * parentChildRelSvc.isChildTable(name)
494-
0 * parentChildRelSvc.isParentTable(name)
515+
1 * parentChildRelSvc.isParentTable(name)
495516
1 * parentChildRelSvc.getParents(name) >> {[] as Set}
496-
2 * parentChildRelSvc.getChildren(name) >> {[] as Set}
517+
1 * parentChildRelSvc.getChildren(name) >> {[] as Set}
497518
1 * config.getNoTableDeleteOnTags() >> []
498519
1 * connectorTableServiceProxy.delete(_)
499520
1 * parentChildRelSvc.drop(_)

metacat-metadata-mysql/src/main/java/com/netflix/metacat/metadata/mysql/MySqlParentChildRelMetaDataService.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@
1717
import org.springframework.transaction.annotation.Transactional;
1818

1919
import java.sql.PreparedStatement;
20-
import java.util.Set;
21-
import java.util.List;
2220
import java.util.ArrayList;
21+
import java.util.List;
22+
import java.util.Set;
23+
import java.util.StringJoiner;
2324
import java.util.HashSet;
2425
import java.util.stream.Collectors;
2526
import java.sql.ResultSet;
@@ -60,6 +61,12 @@ public class MySqlParentChildRelMetaDataService implements ParentChildRelMetadat
6061
static final String SQL_IS_PARENT_TABLE = "SELECT 1 FROM parent_child_relation WHERE parent = ?";
6162
static final String SQL_IS_CHILD_TABLE = "SELECT 1 FROM parent_child_relation WHERE child = ?";
6263

64+
static final String SQL_GET_PARENT_UUIDS = "SELECT DISTINCT parent_uuid FROM parent_child_relation "
65+
+ "where parent = ?";
66+
67+
static final String SQL_GET_CHILDREN_UUIDS = "SELECT DISTINCT child_uuid FROM parent_child_relation "
68+
+ "where child = ?";
69+
6370
private final JdbcTemplate jdbcTemplate;
6471
private final ConverterUtil converterUtil;
6572

@@ -104,6 +111,14 @@ public void createParentChildRelation(final QualifiedName parentName,
104111
+ "- child table: " + childName + " already have child");
105112
}
106113

114+
// Validation to prevent creating a parent with an uuid that is different from the existing parent uuids
115+
final Set<String> existingParentUuids = getExistingUUIDS(parentName.toString());
116+
validateUUIDs(parentName.toString(), existingParentUuids, parentUUID, "Parent");
117+
118+
// Validation to prevent creating a child with an uuid that is different from the existing child uuids
119+
final Set<String> existingChildUuids = getExistingUUIDS(childName.toString());
120+
validateUUIDs(childName.toString(), existingChildUuids, childUUID, "Child");
121+
107122
try {
108123
jdbcTemplate.update(connection -> {
109124
final PreparedStatement ps = connection.prepareStatement(SQL_CREATE_PARENT_CHILD_RELATIONS);
@@ -304,4 +319,48 @@ public Boolean extractData(final ResultSet rs) throws SQLException {
304319
}
305320
);
306321
}
322+
323+
private Set<String> getExistingUUIDS(final String tableName) {
324+
final Set<String> existingUUIDs = new HashSet<>();
325+
existingUUIDs.addAll(getParentUuidsForParent(tableName));
326+
existingUUIDs.addAll(getChildUuidsForChild(tableName));
327+
return existingUUIDs;
328+
}
329+
330+
private Set<String> getParentUuidsForParent(final String parentName) {
331+
return new HashSet<>(jdbcTemplate.query(connection -> {
332+
final PreparedStatement ps = connection.prepareStatement(SQL_GET_PARENT_UUIDS);
333+
ps.setString(1, parentName);
334+
return ps;
335+
}, (rs, rowNum) -> rs.getString("parent_uuid")));
336+
}
337+
338+
private Set<String> getChildUuidsForChild(final String childName) {
339+
return new HashSet<>(jdbcTemplate.query(connection -> {
340+
final PreparedStatement ps = connection.prepareStatement(SQL_GET_CHILDREN_UUIDS);
341+
ps.setString(1, childName);
342+
return ps;
343+
}, (rs, rowNum) -> rs.getString("child_uuid")));
344+
}
345+
346+
private void validateUUIDs(final String name,
347+
final Set<String> existingUUIDs,
348+
final String inputUUID,
349+
final String entity
350+
) throws ParentChildRelServiceException {
351+
if (existingUUIDs.size() > 1 || (!existingUUIDs.isEmpty() && !existingUUIDs.contains(inputUUID))) {
352+
final StringJoiner uuidJoiner = new StringJoiner(", ");
353+
for (String uuid : existingUUIDs) {
354+
uuidJoiner.add(uuid);
355+
}
356+
final String existingUuidsString = uuidJoiner.toString();
357+
358+
throw new ParentChildRelServiceException(
359+
String.format("Cannot create parent-child relation: %s '%s' has existing UUIDs [%s] "
360+
+ "that differ from the input %s UUID '%s'. This normally means table %s already exists",
361+
entity, name, existingUuidsString, entity, inputUUID, name)
362+
);
363+
}
364+
}
365+
307366
}

0 commit comments

Comments
 (0)