Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6d55dac
feat(rdf): SPARQL playground, MCP knowledge-graph tools, insights end…
harshach May 19, 2026
dcdae43
Update generated TypeScript types
github-actions[bot] May 19, 2026
0d69cc5
feat(workflow): add inputPorts, outputPorts, glossaryTerms as Workflo…
yan-3005 May 20, 2026
7eb2702
feat(StringsUtils): add slugify function for string normalization (#2…
chirag-madlani May 20, 2026
3323637
fix edge arrow misalignemt issue (#28276)
anuj-kumary May 20, 2026
609cce0
fix(glossary): register Relations Graph + Data Observability with cus…
harshach May 20, 2026
87d08c7
[codex] Fix Python SDK README examples (#28278)
ayush-shah May 20, 2026
14d45cd
fix(ui): scope between operator fix to numeric custom properties only…
mohitjeswani01 May 20, 2026
3a58a99
feat(context-memory): enforce read-path visibility on memory endpoint…
pmbrull May 20, 2026
0fe4d81
fix(ui): return 404 for unknown service category instead of fallback …
harsh-vador May 20, 2026
57bf667
ci(airflow-apis-tests): migrate Sonar step to sonarqube-scan-action@v…
IceS2 May 20, 2026
3be39b0
fix(logging): synchronous shutdown captures full streamable log tail …
IceS2 May 20, 2026
3737c8e
fix: correct typos (#26784)
hermeztrismez-rgb May 20, 2026
49abf51
feat(mysql): support custom queryHistoryTable for usage & lineage (#2…
ulixius9 May 20, 2026
3064d78
fix(alerts): strict literal matching across alert filter functions (#…
manerow May 20, 2026
eec58fd
fix(ometa): use requests instead of httpx for SSE transport (#28293)
pmbrull May 20, 2026
5114baf
fix: Custom Relation badge always taking default color (#28301)
anuj-kumary May 20, 2026
ef16b03
fix(migration): preempt PDTS duplicates and recover invalid index in …
sonika-shah May 20, 2026
1af4b7f
refactor(data-insights): isolate enricher step failures + per-step En…
manerow May 20, 2026
a3ef84d
feat(ui): allow admins to set default landing-page panel color (#28285)
harshach May 20, 2026
7499688
fix(ui): tier filter queries wrong ES field, showing 0 in DQ widgets …
shah-harshit May 20, 2026
5711162
fix(ui): accept ServiceCategory enum values as :tab in ServicesPage (…
harsh-vador May 20, 2026
4405acb
fix(rdf,migration): use RDF_REMOTE_ENDPOINT env var; surface pending …
harshach May 20, 2026
750d544
Merge remote-tracking branch 'origin/main' into harshach/rdf-fidelity
harshach Jun 17, 2026
a5b6ef9
Fix Java checkstyle
harshach Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package org.openmetadata.it.tests;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import org.awaitility.Awaitility;
Expand All @@ -15,6 +22,7 @@
import org.openmetadata.it.factories.DatabaseSchemaTestFactory;
import org.openmetadata.it.factories.DatabaseServiceTestFactory;
import org.openmetadata.it.util.RdfTestUtils;
import org.openmetadata.it.util.SdkClients;
import org.openmetadata.it.util.TestNamespace;
import org.openmetadata.it.util.TestNamespaceExtension;
import org.openmetadata.schema.api.configuration.rdf.RdfConfiguration;
Expand All @@ -23,6 +31,7 @@
import org.openmetadata.schema.entity.data.Table;
import org.openmetadata.schema.entity.services.DatabaseService;
import org.openmetadata.schema.type.Column;
import org.openmetadata.schema.type.TableConstraint;
import org.openmetadata.sdk.fluent.Tables;
import org.openmetadata.sdk.fluent.builders.ColumnBuilder;
import org.openmetadata.service.rdf.RdfUpdater;
Expand All @@ -43,6 +52,10 @@
public class RdfResourceIT {

private static final String TABLE_RDF_TYPE = "dcat:Dataset";
private static final String BASE_URI = "https://open-metadata.org/";
private static final String OM_NS = BASE_URI + "ontology/";
private static final HttpClient HTTP_CLIENT =
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();

@BeforeAll
static void enableRdf() {
Expand All @@ -51,9 +64,9 @@ static void enableRdf() {
"RDF is disabled for this test run. Use the RDF test profile to execute RdfResourceIT.");
RdfConfiguration rdfConfig = new RdfConfiguration();
rdfConfig.setEnabled(true);
rdfConfig.setBaseUri(java.net.URI.create("https://open-metadata.org/"));
rdfConfig.setBaseUri(URI.create(BASE_URI));
rdfConfig.setStorageType(RdfConfiguration.StorageType.FUSEKI);
rdfConfig.setRemoteEndpoint(java.net.URI.create(TestSuiteBootstrap.getFusekiEndpoint()));
rdfConfig.setRemoteEndpoint(URI.create(TestSuiteBootstrap.getFusekiEndpoint()));
rdfConfig.setUsername("admin");
rdfConfig.setPassword("test-admin");
rdfConfig.setDataset("openmetadata");
Expand Down Expand Up @@ -179,4 +192,251 @@ void testEntityUpdateInRdf(TestNamespace ns) {
.pollInterval(Duration.ofMillis(500))
.untilAsserted(() -> RdfTestUtils.verifyEntityUpdatedInRdf(updated));
}

// ---------------------------------------------------------------------------
// Phase-1 knowledge-graph fidelity tests (P1.1, P1.2, P1.7, P1.9).
// These exercise the new column / constraint / SHACL / ontology behavior
// against the same Fuseki container used by the tests above.
// ---------------------------------------------------------------------------

@Test
void testColumnIsNamedRdfResource(TestNamespace ns) {
DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
DatabaseSchema schema = DatabaseSchemaTestFactory.createSimple(ns, service);

CreateTable createRequest = new CreateTable();
createRequest.setName(ns.prefix("rdfColumnUriTable"));
createRequest.setDatabaseSchema(schema.getFullyQualifiedName());
createRequest.setColumns(
List.of(
ColumnBuilder.of("id", "BIGINT").primaryKey().build(),
ColumnBuilder.of("email", "VARCHAR").dataLength(255).unique().build()));

Table table = Tables.create(createRequest);
assertNotNull(table.getId());

String idColumnFqn = table.getFullyQualifiedName() + ".id";
String emailColumnFqn = table.getFullyQualifiedName() + ".email";

Awaitility.await()
.atMost(Duration.ofSeconds(15))
.pollInterval(Duration.ofMillis(500))
.untilAsserted(
() -> {
assertTrue(
columnIsTypedAsOmColumn(idColumnFqn),
"PK column should be a named om:Column resource at the FQN-derived URI");
assertTrue(
columnIsTypedAsOmColumn(emailColumnFqn),
"UNIQUE column should be a named om:Column resource at the FQN-derived URI");
});
}

@Test
void testColumnConstraintFlagsInRdf(TestNamespace ns) {
DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
DatabaseSchema schema = DatabaseSchemaTestFactory.createSimple(ns, service);

CreateTable createRequest = new CreateTable();
createRequest.setName(ns.prefix("rdfConstraintTable"));
createRequest.setDatabaseSchema(schema.getFullyQualifiedName());
createRequest.setColumns(
List.of(
ColumnBuilder.of("id", "BIGINT").primaryKey().build(),
ColumnBuilder.of("email", "VARCHAR").dataLength(255).unique().build(),
ColumnBuilder.of("country", "VARCHAR").dataLength(2).notNull().build()));

Table table = Tables.create(createRequest);
String idFqn = table.getFullyQualifiedName() + ".id";
String emailFqn = table.getFullyQualifiedName() + ".email";
String countryFqn = table.getFullyQualifiedName() + ".country";

Awaitility.await()
.atMost(Duration.ofSeconds(15))
.pollInterval(Duration.ofMillis(500))
.untilAsserted(
() -> {
assertTrue(
columnHasBooleanProperty(idFqn, "isPrimaryKey", true),
"Primary-key column should set om:isPrimaryKey true");
assertTrue(
columnHasBooleanProperty(idFqn, "isNullable", false),
"Primary-key column should set om:isNullable false");
assertTrue(
columnHasBooleanProperty(idFqn, "isUnique", true),
"Primary-key column should also set om:isUnique true (PKs are unique)");
assertTrue(
columnHasBooleanProperty(emailFqn, "isUnique", true),
"UNIQUE column should set om:isUnique true");
assertTrue(
columnHasBooleanProperty(countryFqn, "isNullable", false),
"NOT_NULL column should set om:isNullable false");
});
}

@Test
void testForeignKeyReferencesInRdf(TestNamespace ns) {
DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
DatabaseSchema schema = DatabaseSchemaTestFactory.createSimple(ns, service);

CreateTable customers = new CreateTable();
customers.setName(ns.prefix("rdfFkCustomers"));
customers.setDatabaseSchema(schema.getFullyQualifiedName());
customers.setColumns(
List.of(
ColumnBuilder.of("id", "BIGINT").primaryKey().build(),
ColumnBuilder.of("name", "VARCHAR").dataLength(255).build()));
Table customersTable = Tables.create(customers);

String customerIdFqn = customersTable.getFullyQualifiedName() + ".id";

CreateTable orders = new CreateTable();
orders.setName(ns.prefix("rdfFkOrders"));
orders.setDatabaseSchema(schema.getFullyQualifiedName());
orders.setColumns(
List.of(
ColumnBuilder.of("order_id", "BIGINT").primaryKey().build(),
ColumnBuilder.of("customer_id", "BIGINT").notNull().build()));

TableConstraint fk =
new TableConstraint()
.withConstraintType(TableConstraint.ConstraintType.FOREIGN_KEY)
.withColumns(List.of("customer_id"))
.withReferredColumns(List.of(customerIdFqn))
.withRelationshipType(TableConstraint.RelationshipType.MANY_TO_ONE);
orders.setTableConstraints(List.of(fk));

Table ordersTable = Tables.create(orders);

String orderCustomerIdFqn = ordersTable.getFullyQualifiedName() + ".customer_id";

Awaitility.await()
.atMost(Duration.ofSeconds(15))
.pollInterval(Duration.ofMillis(500))
.untilAsserted(
() -> {
assertTrue(
columnReferencesColumn(orderCustomerIdFqn, customerIdFqn),
"FOREIGN_KEY constraint should produce direct om:references triple between source and referred column");
assertTrue(
tableHasConstraintOfType(ordersTable.getFullyQualifiedName(), "FOREIGN_KEY"),
"Table should declare a FOREIGN_KEY om:TableConstraint resource");
});
}

@Test
void testOntologyEndpointServesBumpedVersion() throws Exception {
String url = SdkClients.getServerUrl() + "/v1/rdf/ontology";
HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + SdkClients.getAdminToken())
.header("Accept", "text/turtle")
.GET()
.build();
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
assertNotNull(response);
assertTrue(
response.statusCode() == 200,
"GET /v1/rdf/ontology should return 200, got " + response.statusCode());
String body = response.body();
assertTrue(body.contains("1.1.0"), "Ontology document should declare the bumped version 1.1.0");
assertTrue(
body.contains("om:Column") && body.contains("om:TableConstraint"),
"Ontology document should declare core om:Column and om:TableConstraint classes");
}

@Test
void testShaclValidateEndpointReturnsReport(TestNamespace ns) throws Exception {
DatabaseService service = DatabaseServiceTestFactory.createPostgres(ns);
DatabaseSchema schema = DatabaseSchemaTestFactory.createSimple(ns, service);

CreateTable createRequest = new CreateTable();
createRequest.setName(ns.prefix("rdfShaclTable"));
createRequest.setDatabaseSchema(schema.getFullyQualifiedName());
createRequest.setColumns(List.of(ColumnBuilder.of("id", "BIGINT").primaryKey().build()));
Table table = Tables.create(createRequest);

String entityUri = BASE_URI + "entity/table/" + table.getId();
String url =
SdkClients.getServerUrl()
+ "/v1/rdf/validate?entityUri="
+ URLEncoder.encode(entityUri, StandardCharsets.UTF_8);

HttpRequest request =
HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + SdkClients.getAdminToken())
.header("Accept", "text/turtle")
.POST(HttpRequest.BodyPublishers.noBody())
.build();

Awaitility.await()
.atMost(Duration.ofSeconds(15))
.pollInterval(Duration.ofSeconds(1))
.untilAsserted(
() -> {
HttpResponse<String> response =
HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
assertTrue(
response.statusCode() == 200,
"POST /v1/rdf/validate should return 200, got " + response.statusCode());
String body = response.body();
assertTrue(
body.contains("ValidationReport") || body.contains("conforms"),
"Response should be a SHACL validation report");
assertTrue(
response.headers().firstValue("OM-SHACL-Conforms").isPresent(),
"Endpoint should set OM-SHACL-Conforms header");
});
}

// ---- helpers for the fidelity tests ----

private boolean columnIsTypedAsOmColumn(String columnFqn) {
String columnUri =
BASE_URI + "entity/column/" + URLEncoder.encode(columnFqn, StandardCharsets.UTF_8);
String sparql =
String.format(
"PREFIX om: <%s> "
+ "ASK { GRAPH ?g { <%s> a om:Column ; om:fullyQualifiedName \"%s\" } }",
OM_NS, columnUri, columnFqn);
return RdfTestUtils.executeSparqlAsk(sparql);
}

private boolean columnHasBooleanProperty(String columnFqn, String predicate, boolean expected) {
String columnUri =
BASE_URI + "entity/column/" + URLEncoder.encode(columnFqn, StandardCharsets.UTF_8);
String sparql =
String.format(
"PREFIX om: <%s> "
+ "PREFIX xsd: <http://www.w3.org/2001/XMLSchema#> "
+ "ASK { GRAPH ?g { <%s> om:%s \"%s\"^^xsd:boolean } }",
OM_NS, columnUri, predicate, expected);
return RdfTestUtils.executeSparqlAsk(sparql);
}

private boolean columnReferencesColumn(String fromFqn, String toFqn) {
String fromUri =
BASE_URI + "entity/column/" + URLEncoder.encode(fromFqn, StandardCharsets.UTF_8);
String toUri = BASE_URI + "entity/column/" + URLEncoder.encode(toFqn, StandardCharsets.UTF_8);
String sparql =
String.format(
"PREFIX om: <%s> ASK { GRAPH ?g { <%s> om:references <%s> } }", OM_NS, fromUri, toUri);
return RdfTestUtils.executeSparqlAsk(sparql);
}

private boolean tableHasConstraintOfType(String tableFqn, String constraintType) {
String escaped = tableFqn.replace("\\", "\\\\").replace("\"", "\\\"");
String sparql =
String.format(
"PREFIX om: <%s> "
+ "ASK { GRAPH ?g { "
+ " ?table om:fullyQualifiedName \"%s\" ; "
+ " om:hasConstraint ?c . "
+ " ?c om:constraintType \"%s\" . "
+ "} }",
OM_NS, escaped, constraintType);
return RdfTestUtils.executeSparqlAsk(sparql);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,21 @@ public CallToolOutcome callToolWithMetadata(
case "create_data_product":
result = new CreateDataProductTool().execute(authorizer, limits, securityContext, params);
break;
case "sparql_query":
result = new SparqlQueryTool().execute(authorizer, securityContext, params);
break;
case "entity_neighborhood":
result = new EntityNeighborhoodTool().execute(authorizer, securityContext, params);
break;
case "find_by_tag":
result = new FindByTagTool().execute(authorizer, securityContext, params);
break;
case "shacl_validate":
result = new ShaclValidateTool().execute(authorizer, securityContext, params);
break;
case "ontology_describe":
result = new OntologyDescribeTool().execute(authorizer, securityContext, params);
break;
default:
return new CallToolOutcome(
McpSchema.CallToolResult.builder()
Expand Down
Loading
Loading