Skip to content

Commit

Permalink
Fix #12998: Support for Stored Procedures as another entity under Dat…
Browse files Browse the repository at this point in the history
…abase Schema
  • Loading branch information
harshach committed Aug 25, 2023
1 parent 88f2d0e commit d948816
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1816,7 +1816,7 @@ default List<String> listAfter(ListFilter filter, int limit, String after) {
interface StoredProcedureDAO extends EntityDAO<StoredProcedure> {
@Override
default String getTableName() {
return "search_index_entity";
return "stored_procedure_entity";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import static org.openmetadata.service.Entity.STORED_PROCEDURE;

import org.openmetadata.schema.entity.data.DatabaseSchema;
import org.openmetadata.schema.entity.data.SearchIndex;
import org.openmetadata.schema.entity.data.StoredProcedure;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Relationship;
Expand All @@ -16,15 +15,18 @@
import org.openmetadata.service.util.FullyQualifiedName;

public class StoredProcedureRepository extends EntityRepository<StoredProcedure> {
static final String PATCH_FIELDS = "storedProcedureCode";
static final String UPDATE_FIELDS = "storedProcedureCode";

public StoredProcedureRepository(CollectionDAO dao) {
super(
StoredProcedureResource.COLLECTION_PATH,
Entity.SEARCH_INDEX,
STORED_PROCEDURE,
StoredProcedure.class,
dao.storedProcedureDAO(),
dao,
"",
"");
PATCH_FIELDS,
UPDATE_FIELDS);
}

@Override
Expand Down Expand Up @@ -73,7 +75,7 @@ public StoredProcedure setInheritedFields(StoredProcedure storedProcedure, Entit

@Override
public StoredProcedure setFields(StoredProcedure storedProcedure, EntityUtil.Fields fields) {
storedProcedure.setService(getContainer(storedProcedure.getDatabaseSchema().getId()));
setDefaultFields(storedProcedure);
storedProcedure.setFollowers(fields.contains(FIELD_FOLLOWERS) ? getFollowers(storedProcedure) : null);
return storedProcedure;
}
Expand All @@ -83,16 +85,22 @@ public StoredProcedure clearFields(StoredProcedure storedProcedure, EntityUtil.F
return storedProcedure;
}

private void setDefaultFields(StoredProcedure storedProcedure) {
EntityReference schemaRef = getContainer(storedProcedure.getId());
DatabaseSchema schema = Entity.getEntity(schemaRef, "", ALL);
storedProcedure.withDatabaseSchema(schemaRef).withDatabase(schema.getDatabase()).withService(schema.getService());
}

@Override
public StoredProcedureUpdater getUpdater(StoredProcedure original, StoredProcedure updated, Operation operation) {
return new StoredProcedureUpdater(original, updated, operation);
}

public void setService(SearchIndex searchIndex, EntityReference service) {
if (service != null && searchIndex != null) {
public void setService(StoredProcedure storedProcedure, EntityReference service) {
if (service != null && storedProcedure != null) {
addRelationship(
service.getId(), searchIndex.getId(), service.getType(), Entity.SEARCH_INDEX, Relationship.CONTAINS);
searchIndex.setService(service);
service.getId(), storedProcedure.getId(), service.getType(), STORED_PROCEDURE, Relationship.CONTAINS);
storedProcedure.setService(service);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.openmetadata.schema.api.data.RestoreEntity;
import org.openmetadata.schema.entity.data.DatabaseSchema;
import org.openmetadata.schema.entity.data.StoredProcedure;
import org.openmetadata.schema.type.ChangeEvent;
import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.Include;
import org.openmetadata.service.Entity;
Expand All @@ -40,7 +41,7 @@
@Collection(name = "storedProcedures")
public class StoredProcedureResource extends EntityResource<StoredProcedure, StoredProcedureRepository> {
public static final String COLLECTION_PATH = "v1/storedProcedures/";
static final String FIELDS = "owner,usageSummary,tags,extension,domain";
static final String FIELDS = "owner,tags,followers,extension,domain";

@Override
public StoredProcedure addHref(UriInfo uriInfo, StoredProcedure storedProcedure) {
Expand All @@ -55,7 +56,7 @@ public StoredProcedureResource(CollectionDAO dao, Authorizer authorizer) {
super(StoredProcedure.class, new StoredProcedureRepository(dao), authorizer);
}

public static class StoredProcedureList extends ResultList<StoredProcedureList> {
public static class StoredProcedureList extends ResultList<StoredProcedure> {
/* Required for serde */
}

Expand Down Expand Up @@ -283,6 +284,50 @@ public Response createOrUpdate(
return createOrUpdate(uriInfo, securityContext, storedProcedure);
}

@PUT
@Path("/{id}/followers")
@Operation(
operationId = "addFollower",
summary = "Add a follower",
description = "Add a user identified by `userId` as followed of this Stored Procedure",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChangeEvent.class))),
@ApiResponse(responseCode = "404", description = "StoredProcedure for instance {id} is not found")
})
public Response addFollower(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the StoredProcedure", schema = @Schema(type = "UUID")) @PathParam("id") UUID id,
@Parameter(description = "Id of the user to be added as follower", schema = @Schema(type = "UUID")) UUID userId) {
return repository.addFollower(securityContext.getUserPrincipal().getName(), id, userId).toResponse();
}

@DELETE
@Path("/{id}/followers/{userId}")
@Operation(
summary = "Remove a follower",
description = "Remove the user identified `userId` as a follower of the Stored Procedure.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OK",
content = @Content(mediaType = "application/json", schema = @Schema(implementation = ChangeEvent.class)))
})
public Response deleteFollower(
@Context UriInfo uriInfo,
@Context SecurityContext securityContext,
@Parameter(description = "Id of the Stored Procedure", schema = @Schema(type = "UUID")) @PathParam("id") UUID id,
@Parameter(description = "Id of the user being removed as follower", schema = @Schema(type = "string"))
@PathParam("userId")
String userId) {
return repository
.deleteFollower(securityContext.getUserPrincipal().getName(), id, UUID.fromString(userId))
.toResponse();
}

@DELETE
@Path("/{id}")
@Operation(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/*
* Copyright 2021 Collate
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.openmetadata.service.resources.databases;

import static javax.ws.rs.core.Response.Status.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.openmetadata.schema.type.ColumnDataType.*;
import static org.openmetadata.service.Entity.*;
import static org.openmetadata.service.exception.CatalogExceptionMessage.*;
import static org.openmetadata.service.util.EntityUtil.*;
import static org.openmetadata.service.util.TestUtils.*;
import static org.openmetadata.service.util.TestUtils.UpdateType.*;

import java.io.IOException;
import java.util.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.HttpResponseException;
import org.junit.jupiter.api.*;
import org.openmetadata.schema.api.data.*;
import org.openmetadata.schema.entity.data.*;
import org.openmetadata.schema.entity.services.DatabaseService;
import org.openmetadata.schema.type.*;
import org.openmetadata.service.Entity;
import org.openmetadata.service.resources.EntityResourceTest;
import org.openmetadata.service.resources.services.DatabaseServiceResourceTest;
import org.openmetadata.service.resources.tags.TagResourceTest;
import org.openmetadata.service.util.*;
import org.openmetadata.service.util.EntityUtil.*;

@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class StoredProcedureResourceTest extends EntityResourceTest<StoredProcedure, CreateStoredProcedure> {
private final TagResourceTest tagResourceTest = new TagResourceTest();

public StoredProcedureResourceTest() {
super(
STORED_PROCEDURE,
StoredProcedure.class,
StoredProcedureResource.StoredProcedureList.class,
"storedProcedures",
StoredProcedureResource.FIELDS);
supportedNameCharacters = "_'+#- .()$" + EntityResourceTest.RANDOM_STRING_GENERATOR.generate(1);
supportsSearchIndex = true;
}

@Test
void post_storedProcedureWithInvalidDatabase_404(TestInfo test) {
CreateStoredProcedure create = createRequest(test).withDatabaseSchema("nonExistentSchema");
assertResponse(
() -> createEntity(create, ADMIN_AUTH_HEADERS),
NOT_FOUND,
entityNotFound(Entity.DATABASE_SCHEMA, "nonExistentSchema"));
}

@Test
void put_storedProcedureCode_200(TestInfo test) throws IOException {
CreateStoredProcedure createStoredProcedure = createRequest(test);
String query =
"sales_vw\n"
+ "create view sales_vw as\n"
+ "select * from public.sales\n"
+ "union all\n"
+ "select * from spectrum.sales\n"
+ "with no schema binding;\n";
createStoredProcedure.setStoredProcedureCode(
new StoredProcedureCode().withCode(query).withLanguage(StoredProcedureLanguage.SQL));
StoredProcedure storedProcedure = createAndCheckEntity(createStoredProcedure, ADMIN_AUTH_HEADERS);
storedProcedure = getEntity(storedProcedure.getId(), "", ADMIN_AUTH_HEADERS);
assertEquals(storedProcedure.getStoredProcedureCode().getCode(), query);
}

@Test
void patch_storedProcedureCode_200(TestInfo test) throws IOException {
CreateStoredProcedure createStoredProcedure = createRequest(test);
String query =
"sales_vw\n"
+ "create view sales_vw as\n"
+ "select * from public.sales\n"
+ "union all\n"
+ "select * from spectrum.sales\n"
+ "with no schema binding;\n";
createStoredProcedure.setStoredProcedureCode(new StoredProcedureCode().withLanguage(StoredProcedureLanguage.SQL));
StoredProcedure storedProcedure = createAndCheckEntity(createStoredProcedure, ADMIN_AUTH_HEADERS);
String storedProcedureJson = JsonUtils.pojoToJson(storedProcedure);
storedProcedure.setStoredProcedureCode(
new StoredProcedureCode().withLanguage(StoredProcedureLanguage.SQL).withCode(query));
StoredProcedure storedProcedure1 =
patchEntity(storedProcedure.getId(), storedProcedureJson, storedProcedure, ADMIN_AUTH_HEADERS);
compareEntities(storedProcedure, storedProcedure1, ADMIN_AUTH_HEADERS);
StoredProcedure storedProcedure2 = getEntity(storedProcedure.getId(), "", ADMIN_AUTH_HEADERS);
}

@Override
public StoredProcedure validateGetWithDifferentFields(StoredProcedure storedProcedure, boolean byName)
throws HttpResponseException {
storedProcedure =
byName
? getEntityByName(storedProcedure.getFullyQualifiedName(), null, ADMIN_AUTH_HEADERS)
: getEntity(storedProcedure.getId(), null, ADMIN_AUTH_HEADERS);
assertListNotNull(
storedProcedure.getService(),
storedProcedure.getServiceType(),
storedProcedure.getDatabase(),
storedProcedure.getDatabaseSchema(),
storedProcedure.getStoredProcedureCode());
assertListNull(storedProcedure.getOwner(), storedProcedure.getTags(), storedProcedure.getFollowers());

String fields = "owner,tags,followers";
storedProcedure =
byName
? getEntityByName(storedProcedure.getFullyQualifiedName(), fields, ADMIN_AUTH_HEADERS)
: getEntity(storedProcedure.getId(), fields, ADMIN_AUTH_HEADERS);
assertListNotNull(
storedProcedure.getService(),
storedProcedure.getServiceType(),
storedProcedure.getDatabaseSchema(),
storedProcedure.getDatabase());
return storedProcedure;
}

/**
* A method variant to be called form other tests to create a table without depending on Database, DatabaseService set
* up in the {@code setup()} method
*/
public StoredProcedure createEntity(TestInfo test, int index) throws IOException {
DatabaseServiceResourceTest databaseServiceResourceTest = new DatabaseServiceResourceTest();
DatabaseService service =
databaseServiceResourceTest.createEntity(databaseServiceResourceTest.createRequest(test), ADMIN_AUTH_HEADERS);
DatabaseResourceTest databaseResourceTest = new DatabaseResourceTest();
Database database =
databaseResourceTest.createAndCheckEntity(
databaseResourceTest.createRequest(test).withService(service.getFullyQualifiedName()), ADMIN_AUTH_HEADERS);
CreateStoredProcedure create = createRequest(test, index);
return createEntity(create, ADMIN_AUTH_HEADERS).withDatabase(database.getEntityReference());
}

@Override
public CreateStoredProcedure createRequest(String name) {
StoredProcedureCode storedProcedureCode =
new StoredProcedureCode()
.withCode(
"CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\n"
+ "RETURNS VARCHAR NOT NULL\n"
+ "LANGUAGE SQL\n"
+ "AS\n"
+ "BEGIN\n"
+ " RETURN message;\n"
+ "END;")
.withLanguage(StoredProcedureLanguage.SQL);
return new CreateStoredProcedure()
.withName(name)
.withDatabaseSchema(getContainer().getFullyQualifiedName())
.withStoredProcedureCode(storedProcedureCode);
}

@Override
public EntityReference getContainer() {
return DATABASE_SCHEMA.getEntityReference();
}

@Override
public EntityReference getContainer(StoredProcedure entity) {
return entity.getDatabaseSchema();
}

@Override
public void validateCreatedEntity(
StoredProcedure createdEntity, CreateStoredProcedure createRequest, Map<String, String> authHeaders)
throws HttpResponseException {
// Entity specific validation
assertReference(createRequest.getDatabaseSchema(), createdEntity.getDatabaseSchema());
validateEntityReference(createdEntity.getDatabase());
validateEntityReference(createdEntity.getService());
TestUtils.validateTags(createRequest.getTags(), createdEntity.getTags());
TestUtils.validateEntityReferences(createdEntity.getFollowers());
assertListNotNull(createdEntity.getService(), createdEntity.getServiceType());
assertEquals(createdEntity.getStoredProcedureCode(), createRequest.getStoredProcedureCode());
assertEquals(
FullyQualifiedName.add(createdEntity.getDatabaseSchema().getFullyQualifiedName(), createdEntity.getName()),
createdEntity.getFullyQualifiedName());
}

@Override
public void compareEntities(StoredProcedure expected, StoredProcedure patched, Map<String, String> authHeaders)
throws HttpResponseException {
// Entity specific validation
validateDatabase(expected.getDatabase(), patched.getDatabase());
TestUtils.validateTags(expected.getTags(), patched.getTags());
TestUtils.validateEntityReferences(expected.getFollowers());
assertEquals(expected.getStoredProcedureCode(), patched.getStoredProcedureCode());
assertEquals(
FullyQualifiedName.add(patched.getDatabaseSchema().getFullyQualifiedName(), patched.getName()),
patched.getFullyQualifiedName());
}

private void validateDatabase(EntityReference expectedDatabase, EntityReference database) {
TestUtils.validateEntityReference(database);
assertEquals(expectedDatabase.getId(), database.getId());
}

@Override
public void assertFieldChange(String fieldName, Object expected, Object actual) throws IOException {
if (expected == actual) {
return;
}
if (fieldName.startsWith("storedProcedureCode")) {
StoredProcedureCode expectedCode = (StoredProcedureCode) expected;
StoredProcedureCode actualCode = (StoredProcedureCode) actual;
assertEquals(expectedCode, actualCode);
} else {
assertCommonFieldChange(fieldName, expected, actual);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"properties": {
"name": {
"description": "Name of a Stored Procedure.",
"$ref": "../../type/basic.json#/definitions/entityName"
"$ref": "../../entity/data/storedProcedure.json#/definitions/entityName"
},
"displayName": {
"description": "Display Name that identifies this Stored Procedure.",
Expand Down
Loading

0 comments on commit d948816

Please sign in to comment.