jerryshao commented on code in PR #11036:
URL: https://github.com/apache/gravitino/pull/11036#discussion_r3232248582
##########
core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/OwnerMetaBaseSQLProvider.java:
##########
@@ -124,6 +125,35 @@ public String insertOwnerRel(@Param("ownerRelPO")
OwnerRelPO ownerRelPO) {
+ ")";
}
+ public String batchInsertOwnerRels(@Param("ownerRelPOs") List<OwnerRelPO>
ownerRelPOs) {
+ return "<script>"
+ + "INSERT INTO "
+ + OWNER_TABLE_NAME
+ + " (metalake_id, metadata_object_id, metadata_object_type, owner_id,
owner_type,"
+ + " audit_info, current_version, last_version, deleted_at, updated_at)
VALUES "
+ + "<foreach collection='ownerRelPOs' item='po' separator=','>"
+ + "(#{po.metalakeId}, #{po.metadataObjectId},
#{po.metadataObjectType},"
+ + " #{po.ownerId}, #{po.ownerType}, #{po.auditInfo},"
+ + " #{po.currentVersion}, #{po.lastVersion}, #{po.deletedAt},
#{po.updatedAt})"
+ + "</foreach>"
+ + "</script>";
+ }
+
+ public String batchSoftDeleteOwnerRelByMetadataObjects(
+ @Param("deletions") List<OwnerRelForDeletion> deletions) {
+ return "<script>"
+ + "UPDATE "
+ + OWNER_TABLE_NAME
+ + " SET deleted_at = (UNIX_TIMESTAMP() * 1000.0)"
Review Comment:
**MySQL soft-delete timestamp produces a `DOUBLE`, not `BIGINT`**
`UNIX_TIMESTAMP()` (no argument) returns a plain integer — no sub-second
precision. Adding `EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000` makes
the expression a `DOUBLE`, which is stored into the `BIGINT` `deleted_at`
column via an implicit cast. MySQL silently truncates, but this is fragile and
inconsistent with the PostgreSQL override which uses `CAST(... AS BIGINT)`
explicitly.
Consider wrapping with `FLOOR()` to make the truncation explicit:
```sql
SET deleted_at = FLOOR((UNIX_TIMESTAMP() * 1000.0)
+ EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(3)) / 1000)
```
##########
core/src/main/java/org/apache/gravitino/storage/relational/service/SchemaMetaService.java:
##########
@@ -162,18 +169,69 @@ public List<SchemaEntity>
listSchemasByNamespace(Namespace namespace) {
public void insertSchema(SchemaEntity schemaEntity, boolean overwrite)
throws IOException {
try {
NameIdentifierUtil.checkSchema(schemaEntity.nameIdentifier());
-
- SchemaPO.Builder builder = SchemaPO.builder();
- fillSchemaPOBuilderParentEntityId(builder, schemaEntity.namespace());
+ // Callers above this service (e.g. JDBCBackend + naming bridge) pass
storage-form schema
+ // names: nested paths use the internal physical separator, not the
external logical one.
+ String physicalSep = HierarchicalSchemaUtil.physicalSeparator();
+ String schemaName = schemaEntity.name();
+ List<SchemaEntity> rowsToInsert = new ArrayList<>();
+ if (schemaName == null || !schemaName.contains(physicalSep)) {
+ rowsToInsert.add(schemaEntity);
+ } else {
+ String[] parts = schemaName.split(Pattern.quote(physicalSep), -1);
+ for (int nSeg = 1; nSeg < parts.length; nSeg++) {
+ String ancestorPhysical = String.join(physicalSep,
Arrays.copyOf(parts, nSeg));
+ SchemaEntity ancestor =
+ SchemaEntity.builder()
+ .withId(nextIdForNestedAncestor())
+ .withName(ancestorPhysical)
+ .withNamespace(schemaEntity.namespace())
+ .withComment(null)
+ .withProperties(Collections.emptyMap())
+ .withAuditInfo(schemaEntity.auditInfo())
+ .build();
+ rowsToInsert.add(ancestor);
+ }
+ rowsToInsert.add(schemaEntity);
+ }
SessionUtils.doWithCommit(
SchemaMetaMapper.class,
mapper -> {
- SchemaPO po =
POConverters.initializeSchemaPOWithVersion(schemaEntity, builder);
+ int n = rowsToInsert.size();
+ List<SchemaPO> missingAncestorPOs = new ArrayList<>();
+ if (n > 1) {
+ SchemaEntity firstAncestor = rowsToInsert.get(0);
+ Namespace ancestorNs = firstAncestor.namespace();
+ List<String> ancestorPhysicalNames =
+ rowsToInsert.subList(0, n - 1).stream()
+ .map(SchemaEntity::name)
+ .collect(Collectors.toList());
+ Set<String> existingAncestorNames =
+ mapper
+ .batchSelectSchemaByIdentifier(
+ ancestorNs.level(0), ancestorNs.level(1),
ancestorPhysicalNames)
+ .stream()
+ .map(SchemaPO::getSchemaName)
+ .collect(Collectors.toSet());
+ for (SchemaEntity row : rowsToInsert.subList(0, n - 1)) {
+ if (existingAncestorNames.contains(row.name())) {
+ continue;
+ }
+ SchemaPO.Builder builder = SchemaPO.builder();
+ fillSchemaPOBuilderParentEntityId(builder, row.namespace());
+
missingAncestorPOs.add(POConverters.initializeSchemaPOWithVersion(row,
builder));
+ }
+ }
+ SchemaEntity leafRow = rowsToInsert.get(n - 1);
+ SchemaPO.Builder leafBuilder = SchemaPO.builder();
+ fillSchemaPOBuilderParentEntityId(leafBuilder,
leafRow.namespace());
+ SchemaPO leafPO =
POConverters.initializeSchemaPOWithVersion(leafRow, leafBuilder);
+ List<SchemaPO> schemaPosToInsert = new
ArrayList<>(missingAncestorPOs);
+ schemaPosToInsert.add(leafPO);
if (overwrite) {
- mapper.insertSchemaMetaOnDuplicateKeyUpdate(po);
+
mapper.batchInsertSchemaMetaOnDuplicateKeyUpdate(schemaPosToInsert);
} else {
- mapper.insertSchemaMeta(po);
+ mapper.batchInsertSchemaMeta(schemaPosToInsert);
Review Comment:
**Ancestor insert race condition when `overwrite=false`**
Two concurrent inserts of `A:B:C` and `A:B:D` will both call
`batchSelectSchemaByIdentifier`, both find ancestor `A` missing, and both
attempt `batchInsertSchemaMeta`. The second caller gets a duplicate-key
exception. The `overwrite=true` path handles this correctly via upsert, but the
`false` path does not.
Catalog bootstrap scenarios (where many hierarchical schemas are imported in
parallel) are particularly vulnerable. At minimum, document the known
limitation. Ideally, catch the duplicate-key exception on ancestor rows
specifically and treat it as a benign race.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]