|
25 | 25 | import jakarta.annotation.Nullable; |
26 | 26 | import java.sql.Connection; |
27 | 27 | import java.sql.SQLException; |
| 28 | +import java.time.Instant; |
28 | 29 | import java.util.ArrayList; |
29 | 30 | import java.util.Collections; |
30 | 31 | import java.util.HashMap; |
|
33 | 34 | import java.util.Map; |
34 | 35 | import java.util.Objects; |
35 | 36 | import java.util.Optional; |
| 37 | +import java.util.OptionalLong; |
36 | 38 | import java.util.concurrent.atomic.AtomicReference; |
37 | 39 | import java.util.function.Function; |
38 | 40 | import java.util.function.Predicate; |
39 | 41 | import java.util.stream.Collectors; |
40 | 42 | import org.apache.polaris.core.PolarisCallContext; |
41 | 43 | import org.apache.polaris.core.PolarisDiagnostics; |
| 44 | +import org.apache.polaris.core.context.RealmContext; |
42 | 45 | import org.apache.polaris.core.entity.EntityNameLookupRecord; |
43 | 46 | import org.apache.polaris.core.entity.LocationBasedEntity; |
44 | 47 | import org.apache.polaris.core.entity.PolarisBaseEntity; |
|
51 | 54 | import org.apache.polaris.core.entity.PolarisEvent; |
52 | 55 | import org.apache.polaris.core.entity.PolarisGrantRecord; |
53 | 56 | import org.apache.polaris.core.entity.PolarisPrincipalSecrets; |
| 57 | +import org.apache.polaris.core.lineage.LineageColumnEdge; |
| 58 | +import org.apache.polaris.core.lineage.LineageData; |
| 59 | +import org.apache.polaris.core.lineage.LineageDataset; |
| 60 | +import org.apache.polaris.core.lineage.LineageDirection; |
| 61 | +import org.apache.polaris.core.lineage.LineageEdge; |
| 62 | +import org.apache.polaris.core.lineage.LineageFieldMapping; |
| 63 | +import org.apache.polaris.core.lineage.LineageGranularity; |
| 64 | +import org.apache.polaris.core.lineage.LineageGraph; |
| 65 | +import org.apache.polaris.core.lineage.LineageNode; |
| 66 | +import org.apache.polaris.core.lineage.LineageNodeType; |
| 67 | +import org.apache.polaris.core.lineage.LineagePersistence; |
| 68 | +import org.apache.polaris.core.lineage.LineageQueryRequest; |
54 | 69 | import org.apache.polaris.core.persistence.BaseMetaStoreManager; |
55 | 70 | import org.apache.polaris.core.persistence.BasePersistence; |
56 | 71 | import org.apache.polaris.core.persistence.EntityAlreadyExistsException; |
57 | 72 | import org.apache.polaris.core.persistence.IntegrationPersistence; |
58 | 73 | import org.apache.polaris.core.persistence.PolicyMappingAlreadyExistsException; |
59 | 74 | import org.apache.polaris.core.persistence.PrincipalSecretsGenerator; |
60 | 75 | import org.apache.polaris.core.persistence.RetryOnConcurrencyException; |
61 | | -import org.apache.polaris.core.persistence.lineage.LineageColumnEdgeRecord; |
62 | | -import org.apache.polaris.core.persistence.lineage.LineageDatasetRecord; |
63 | | -import org.apache.polaris.core.persistence.lineage.LineageEdgeRecord; |
64 | 76 | import org.apache.polaris.core.persistence.metrics.CommitMetricsRecord; |
65 | 77 | import org.apache.polaris.core.persistence.metrics.ScanMetricsRecord; |
66 | 78 | import org.apache.polaris.core.persistence.pagination.EntityIdToken; |
|
73 | 85 | import org.apache.polaris.core.storage.PolarisStorageIntegration; |
74 | 86 | import org.apache.polaris.core.storage.PolarisStorageIntegrationProvider; |
75 | 87 | import org.apache.polaris.core.storage.StorageLocation; |
| 88 | +import org.apache.polaris.persistence.relational.jdbc.models.Converter; |
76 | 89 | import org.apache.polaris.persistence.relational.jdbc.models.EntityNameLookupRecordConverter; |
77 | 90 | import org.apache.polaris.persistence.relational.jdbc.models.ModelCommitMetricsReport; |
78 | 91 | import org.apache.polaris.persistence.relational.jdbc.models.ModelEntity; |
|
88 | 101 | import org.slf4j.Logger; |
89 | 102 | import org.slf4j.LoggerFactory; |
90 | 103 |
|
91 | | -public class JdbcBasePersistenceImpl implements BasePersistence, IntegrationPersistence { |
| 104 | +public class JdbcBasePersistenceImpl |
| 105 | + implements BasePersistence, IntegrationPersistence, LineagePersistence { |
92 | 106 |
|
93 | 107 | private static final Logger LOGGER = LoggerFactory.getLogger(JdbcBasePersistenceImpl.class); |
94 | 108 |
|
@@ -1365,41 +1379,232 @@ private void writeCommitMetricsReport(@Nonnull ModelCommitMetricsReport report) |
1365 | 1379 | // ============================================================================ |
1366 | 1380 |
|
1367 | 1381 | @Override |
1368 | | - public void upsertLineageDataset(@Nonnull LineageDatasetRecord record) { |
| 1382 | + public void upsertDatasets(RealmContext realmContext, List<LineageDataset> datasets) { |
1369 | 1383 | verifyLineagePersistenceSupported(); |
1370 | | - ModelLineageDataset model = ModelLineageDataset.fromRecord(record, realmId); |
| 1384 | + String realmId = realmContext.getRealmIdentifier(); |
| 1385 | + long nowMillis = Instant.now().toEpochMilli(); |
| 1386 | + for (LineageDataset dataset : datasets) { |
| 1387 | + long datasetId = |
| 1388 | + lookupLineageDataset(realmId, dataset) |
| 1389 | + .map(ModelLineageDataset::getDatasetId) |
| 1390 | + .orElseGet(IdGenerator.getIdGenerator()::nextId); |
| 1391 | + ModelLineageDataset model = |
| 1392 | + ModelLineageDataset.fromDataset(dataset, realmId, datasetId, nowMillis); |
| 1393 | + try { |
| 1394 | + datasourceOperations.executeUpdate(generateLineageDatasetUpsert(model)); |
| 1395 | + } catch (SQLException e) { |
| 1396 | + throw new RuntimeException( |
| 1397 | + String.format("Failed to upsert lineage dataset due to %s", e.getMessage()), e); |
| 1398 | + } |
| 1399 | + } |
| 1400 | + } |
| 1401 | + |
| 1402 | + @Override |
| 1403 | + public void upsertDatasetEdges( |
| 1404 | + RealmContext realmContext, List<LineageEdge> edges, Instant lastEventAt) { |
| 1405 | + verifyLineagePersistenceSupported(); |
| 1406 | + String realmId = realmContext.getRealmIdentifier(); |
| 1407 | + long lastEventAtMillis = lastEventAt.toEpochMilli(); |
| 1408 | + for (LineageEdge edge : edges) { |
| 1409 | + ModelLineageEdge model = |
| 1410 | + ModelLineageEdge.fromIds( |
| 1411 | + realmId, |
| 1412 | + requireLineageDatasetId(realmId, edge.source()), |
| 1413 | + requireLineageDatasetId(realmId, edge.target()), |
| 1414 | + lastEventAtMillis); |
| 1415 | + try { |
| 1416 | + datasourceOperations.executeUpdate(generateLineageEdgeUpsert(model)); |
| 1417 | + } catch (SQLException e) { |
| 1418 | + throw new RuntimeException( |
| 1419 | + String.format("Failed to upsert lineage edge due to %s", e.getMessage()), e); |
| 1420 | + } |
| 1421 | + } |
| 1422 | + } |
| 1423 | + |
| 1424 | + @Override |
| 1425 | + public void upsertColumnEdges( |
| 1426 | + RealmContext realmContext, List<LineageColumnEdge> columnEdges, Instant lastEventAt) { |
| 1427 | + verifyLineagePersistenceSupported(); |
| 1428 | + String realmId = realmContext.getRealmIdentifier(); |
| 1429 | + long lastEventAtMillis = lastEventAt.toEpochMilli(); |
| 1430 | + for (LineageColumnEdge columnEdge : columnEdges) { |
| 1431 | + ModelLineageColumnEdge model = |
| 1432 | + ModelLineageColumnEdge.fromIds( |
| 1433 | + realmId, |
| 1434 | + requireLineageDatasetId(realmId, columnEdge.source().dataset()), |
| 1435 | + columnEdge.source().field(), |
| 1436 | + requireLineageDatasetId(realmId, columnEdge.target().dataset()), |
| 1437 | + columnEdge.target().field(), |
| 1438 | + lastEventAtMillis); |
| 1439 | + try { |
| 1440 | + datasourceOperations.executeUpdate(generateLineageColumnEdgeUpsert(model)); |
| 1441 | + } catch (SQLException e) { |
| 1442 | + throw new RuntimeException( |
| 1443 | + String.format("Failed to upsert lineage column edge due to %s", e.getMessage()), e); |
| 1444 | + } |
| 1445 | + } |
| 1446 | + } |
| 1447 | + |
| 1448 | + @Override |
| 1449 | + public LineageGraph loadLineage(RealmContext realmContext, LineageQueryRequest request) { |
| 1450 | + verifyLineagePersistenceSupported(); |
| 1451 | + String realmId = realmContext.getRealmIdentifier(); |
| 1452 | + Optional<ModelLineageDataset> requested = |
| 1453 | + lookupLineageDatasetByNodeId(realmId, request.nodeId()); |
| 1454 | + if (requested.isEmpty()) { |
| 1455 | + return new LineageGraph( |
| 1456 | + new LineageNode(request.nodeId(), LineageNodeType.DATASET, null, true), |
| 1457 | + List.of(), |
| 1458 | + List.of()); |
| 1459 | + } |
| 1460 | + |
| 1461 | + ModelLineageDataset dataset = requested.get(); |
| 1462 | + boolean includeColumns = request.granularity() == LineageGranularity.COLUMN; |
| 1463 | + List<LineageNode> upstream = |
| 1464 | + request.direction() == LineageDirection.UPSTREAM |
| 1465 | + || request.direction() == LineageDirection.BOTH |
| 1466 | + ? loadAdjacentLineageNodes(realmId, dataset.getDatasetId(), true, includeColumns) |
| 1467 | + : List.of(); |
| 1468 | + List<LineageNode> downstream = |
| 1469 | + request.direction() == LineageDirection.DOWNSTREAM |
| 1470 | + || request.direction() == LineageDirection.BOTH |
| 1471 | + ? loadAdjacentLineageNodes(realmId, dataset.getDatasetId(), false, includeColumns) |
| 1472 | + : List.of(); |
| 1473 | + |
| 1474 | + return new LineageGraph(toLineageNode(dataset, List.of()), upstream, downstream); |
| 1475 | + } |
| 1476 | + |
| 1477 | + private Optional<ModelLineageDataset> lookupLineageDataset( |
| 1478 | + String realmId, LineageDataset dataset) { |
| 1479 | + String table = QueryGenerator.getFullyQualifiedTableName(ModelLineageDataset.TABLE_NAME); |
| 1480 | + PreparedQuery query = |
| 1481 | + new PreparedQuery( |
| 1482 | + "SELECT realm_id, dataset_id, catalog, namespace, name, polaris_entity_id, created_at, updated_at " |
| 1483 | + + "FROM " |
| 1484 | + + table |
| 1485 | + + " WHERE realm_id = ? AND namespace = ? AND name = ?", |
| 1486 | + List.of(realmId, dataset.namespace(), dataset.name())); |
1371 | 1487 | try { |
1372 | | - datasourceOperations.executeUpdate(generateLineageDatasetUpsert(model)); |
| 1488 | + List<ModelLineageDataset> results = |
| 1489 | + datasourceOperations.executeSelect(query, ModelLineageDataset.CONVERTER); |
| 1490 | + return results.stream().findFirst(); |
1373 | 1491 | } catch (SQLException e) { |
1374 | 1492 | throw new RuntimeException( |
1375 | | - String.format("Failed to upsert lineage dataset due to %s", e.getMessage()), e); |
| 1493 | + String.format("Failed to load lineage dataset due to %s", e.getMessage()), e); |
1376 | 1494 | } |
1377 | 1495 | } |
1378 | 1496 |
|
1379 | | - @Override |
1380 | | - public void upsertLineageEdge(@Nonnull LineageEdgeRecord record) { |
1381 | | - verifyLineagePersistenceSupported(); |
1382 | | - ModelLineageEdge model = ModelLineageEdge.fromRecord(record, realmId); |
| 1497 | + private Optional<ModelLineageDataset> lookupLineageDatasetByNodeId( |
| 1498 | + String realmId, String nodeId) { |
| 1499 | + return parseLineageDatasetNodeId(nodeId) |
| 1500 | + .flatMap(dataset -> lookupLineageDataset(realmId, dataset)); |
| 1501 | + } |
| 1502 | + |
| 1503 | + private Optional<LineageDataset> parseLineageDatasetNodeId(String nodeId) { |
| 1504 | + String prefix = "dataset:"; |
| 1505 | + if (!nodeId.startsWith(prefix)) { |
| 1506 | + return Optional.empty(); |
| 1507 | + } |
| 1508 | + String identity = nodeId.substring(prefix.length()); |
| 1509 | + int catalogSeparator = identity.lastIndexOf(':'); |
| 1510 | + int nameSeparator = identity.lastIndexOf('.'); |
| 1511 | + if (catalogSeparator < 0 || nameSeparator <= catalogSeparator + 1) { |
| 1512 | + return Optional.empty(); |
| 1513 | + } |
| 1514 | + String catalog = identity.substring(0, catalogSeparator); |
| 1515 | + String namespace = identity.substring(catalogSeparator + 1, nameSeparator); |
| 1516 | + String name = identity.substring(nameSeparator + 1); |
| 1517 | + return Optional.of(new LineageDataset(catalog, namespace, name)); |
| 1518 | + } |
| 1519 | + |
| 1520 | + private long requireLineageDatasetId(String realmId, LineageDataset dataset) { |
| 1521 | + return lookupLineageDataset(realmId, dataset) |
| 1522 | + .map(ModelLineageDataset::getDatasetId) |
| 1523 | + .orElseThrow( |
| 1524 | + () -> |
| 1525 | + new IllegalArgumentException( |
| 1526 | + String.format( |
| 1527 | + "Lineage dataset '%s.%s' does not exist in realm '%s'. Call upsertDatasets before writing edges.", |
| 1528 | + dataset.namespace(), dataset.name(), realmId))); |
| 1529 | + } |
| 1530 | + |
| 1531 | + private List<LineageNode> loadAdjacentLineageNodes( |
| 1532 | + String realmId, long datasetId, boolean upstream, boolean includeColumns) { |
| 1533 | + String edgesTable = QueryGenerator.getFullyQualifiedTableName(ModelLineageEdge.TABLE_NAME); |
| 1534 | + String datasetsTable = |
| 1535 | + QueryGenerator.getFullyQualifiedTableName(ModelLineageDataset.TABLE_NAME); |
| 1536 | + String adjacentEdgeColumn = upstream ? "source_dataset_id" : "target_dataset_id"; |
| 1537 | + String requestedEdgeColumn = upstream ? "target_dataset_id" : "source_dataset_id"; |
| 1538 | + PreparedQuery query = |
| 1539 | + new PreparedQuery( |
| 1540 | + "SELECT d.realm_id, d.dataset_id, d.catalog, d.namespace, d.name, d.polaris_entity_id, d.created_at, d.updated_at " |
| 1541 | + + "FROM " |
| 1542 | + + edgesTable |
| 1543 | + + " e JOIN " |
| 1544 | + + datasetsTable |
| 1545 | + + " d ON d.realm_id = e.realm_id AND d.dataset_id = e." |
| 1546 | + + adjacentEdgeColumn |
| 1547 | + + " WHERE e.realm_id = ? AND e." |
| 1548 | + + requestedEdgeColumn |
| 1549 | + + " = ?", |
| 1550 | + List.of(realmId, datasetId)); |
1383 | 1551 | try { |
1384 | | - datasourceOperations.executeUpdate(generateLineageEdgeUpsert(model)); |
| 1552 | + List<ModelLineageDataset> datasets = |
| 1553 | + datasourceOperations.executeSelect(query, ModelLineageDataset.CONVERTER); |
| 1554 | + return datasets.stream() |
| 1555 | + .map( |
| 1556 | + dataset -> |
| 1557 | + toLineageNode( |
| 1558 | + dataset, |
| 1559 | + includeColumns |
| 1560 | + ? loadFieldMappings(realmId, datasetId, dataset.getDatasetId(), upstream) |
| 1561 | + : List.of())) |
| 1562 | + .toList(); |
1385 | 1563 | } catch (SQLException e) { |
1386 | 1564 | throw new RuntimeException( |
1387 | | - String.format("Failed to upsert lineage edge due to %s", e.getMessage()), e); |
| 1565 | + String.format("Failed to load adjacent lineage nodes due to %s", e.getMessage()), e); |
1388 | 1566 | } |
1389 | 1567 | } |
1390 | 1568 |
|
1391 | | - @Override |
1392 | | - public void upsertLineageColumnEdge(@Nonnull LineageColumnEdgeRecord record) { |
1393 | | - verifyLineagePersistenceSupported(); |
1394 | | - ModelLineageColumnEdge model = ModelLineageColumnEdge.fromRecord(record, realmId); |
| 1569 | + private List<LineageFieldMapping> loadFieldMappings( |
| 1570 | + String realmId, long requestedDatasetId, long adjacentDatasetId, boolean upstream) { |
| 1571 | + String table = QueryGenerator.getFullyQualifiedTableName(ModelLineageColumnEdge.TABLE_NAME); |
| 1572 | + long sourceDatasetId = upstream ? adjacentDatasetId : requestedDatasetId; |
| 1573 | + long targetDatasetId = upstream ? requestedDatasetId : adjacentDatasetId; |
| 1574 | + PreparedQuery query = |
| 1575 | + new PreparedQuery( |
| 1576 | + "SELECT source_field, target_field FROM " |
| 1577 | + + table |
| 1578 | + + " WHERE realm_id = ? AND source_dataset_id = ? AND target_dataset_id = ?", |
| 1579 | + List.of(realmId, sourceDatasetId, targetDatasetId)); |
1395 | 1580 | try { |
1396 | | - datasourceOperations.executeUpdate(generateLineageColumnEdgeUpsert(model)); |
| 1581 | + return datasourceOperations.executeSelect(query, new LineageFieldMappingConverter()); |
1397 | 1582 | } catch (SQLException e) { |
1398 | 1583 | throw new RuntimeException( |
1399 | | - String.format("Failed to upsert lineage column edge due to %s", e.getMessage()), e); |
| 1584 | + String.format("Failed to load lineage field mappings due to %s", e.getMessage()), e); |
1400 | 1585 | } |
1401 | 1586 | } |
1402 | 1587 |
|
| 1588 | + private static LineageNode toLineageNode( |
| 1589 | + ModelLineageDataset dataset, List<LineageFieldMapping> fieldMappings) { |
| 1590 | + LineageData data = |
| 1591 | + new LineageData( |
| 1592 | + OptionalLong.empty(), |
| 1593 | + OptionalLong.of(dataset.getDatasetId()), |
| 1594 | + dataset.getNamespace(), |
| 1595 | + dataset.getName(), |
| 1596 | + null, |
| 1597 | + OptionalLong.of(dataset.getCreatedAt()), |
| 1598 | + OptionalLong.of(dataset.getUpdatedAt())); |
| 1599 | + return new LineageNode( |
| 1600 | + lineageNodeId(dataset), LineageNodeType.DATASET, data, false, fieldMappings); |
| 1601 | + } |
| 1602 | + |
| 1603 | + private static String lineageNodeId(ModelLineageDataset dataset) { |
| 1604 | + return String.format( |
| 1605 | + "dataset:%s:%s.%s", dataset.getCatalog(), dataset.getNamespace(), dataset.getName()); |
| 1606 | + } |
| 1607 | + |
1403 | 1608 | private void verifyLineagePersistenceSupported() { |
1404 | 1609 | if (schemaVersion >= MIN_LINEAGE_SCHEMA_VERSION) { |
1405 | 1610 | return; |
@@ -1516,4 +1721,16 @@ private PreparedQuery generateLineageColumnEdgeUpsert(ModelLineageColumnEdge mod |
1516 | 1721 | + ".last_event_at, EXCLUDED.last_event_at)", |
1517 | 1722 | values); |
1518 | 1723 | } |
| 1724 | + |
| 1725 | + private static class LineageFieldMappingConverter implements Converter<LineageFieldMapping> { |
| 1726 | + @Override |
| 1727 | + public LineageFieldMapping fromResultSet(java.sql.ResultSet rs) throws SQLException { |
| 1728 | + return new LineageFieldMapping(rs.getString("source_field"), rs.getString("target_field")); |
| 1729 | + } |
| 1730 | + |
| 1731 | + @Override |
| 1732 | + public Map<String, Object> toMap(DatabaseType databaseType) { |
| 1733 | + return Map.of(); |
| 1734 | + } |
| 1735 | + } |
1519 | 1736 | } |
0 commit comments