diff --git a/bootstrap/sql/migrations/native/1.2.0/mysql/schemaChanges.sql b/bootstrap/sql/migrations/native/1.2.0/mysql/schemaChanges.sql index e220386db907..4555541e0c20 100644 --- a/bootstrap/sql/migrations/native/1.2.0/mysql/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.2.0/mysql/schemaChanges.sql @@ -57,3 +57,17 @@ CREATE TABLE IF NOT EXISTS search_index_entity ( UPDATE ingestion_pipeline_entity SET json = JSON_REPLACE(json, '$.airflowConfig.retries', 0) WHERE JSON_EXTRACT(json, '$.airflowConfig.retries') IS NOT NULL; + + +-- create stored procedure entity +CREATE TABLE IF NOT EXISTS stored_procedure_entity ( + id VARCHAR(36) GENERATED ALWAYS AS (json ->> '$.id') STORED NOT NULL, + name VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.name') NOT NULL, + fqnHash VARCHAR(256) NOT NULL COLLATE ascii_bin, + json JSON NOT NULL, + updatedAt BIGINT UNSIGNED GENERATED ALWAYS AS (json ->> '$.updatedAt') NOT NULL, + updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> '$.updatedBy') NOT NULL, + deleted BOOLEAN GENERATED ALWAYS AS (json -> '$.deleted'), + PRIMARY KEY (id), + UNIQUE (fqnHash) +); \ No newline at end of file diff --git a/bootstrap/sql/migrations/native/1.2.0/postgres/schemaChanges.sql b/bootstrap/sql/migrations/native/1.2.0/postgres/schemaChanges.sql index f986d709973b..fd67f755a369 100644 --- a/bootstrap/sql/migrations/native/1.2.0/postgres/schemaChanges.sql +++ b/bootstrap/sql/migrations/native/1.2.0/postgres/schemaChanges.sql @@ -56,3 +56,16 @@ CREATE TABLE IF NOT EXISTS search_index_entity ( -- We were hardcoding retries to 0. Since we are now using the IngestionPipeline to set them, keep existing ones to 0. UPDATE ingestion_pipeline_entity SET json = jsonb_set(json::jsonb, '{airflowConfig,retries}', '0', true); + +-- create stored procedure entity +CREATE TABLE IF NOT EXISTS stored_procedure_entity ( + id VARCHAR(36) GENERATED ALWAYS AS (json ->> 'id') STORED NOT NULL, + name VARCHAR(256) GENERATED ALWAYS AS (json ->> 'name') STORED NOT NULL, + fqnHash VARCHAR(256) NOT NULL, + json JSONB NOT NULL, + updatedAt BIGINT GENERATED ALWAYS AS ((json ->> 'updatedAt')::bigint) STORED NOT NULL, + updatedBy VARCHAR(256) GENERATED ALWAYS AS (json ->> 'updatedBy') STORED NOT NULL, + deleted BOOLEAN GENERATED ALWAYS AS ((json ->> 'deleted')::boolean) STORED, + PRIMARY KEY (id), + UNIQUE (fqnHash) + ); \ No newline at end of file diff --git a/ingestion/examples/sample_data/datasets/stored_procedures.json b/ingestion/examples/sample_data/datasets/stored_procedures.json new file mode 100644 index 000000000000..5c56a7e022fa --- /dev/null +++ b/ingestion/examples/sample_data/datasets/stored_procedures.json @@ -0,0 +1,60 @@ +{ + "storedProcedures": [ + { + "name": "update_dim_address_table", + "description": "This stored procedure updates dim_address table", + "version": 0.1, + "updatedAt": 1638354087391, + "updatedBy": "anonymous", + "href": "http://localhost:8585/api/v1/tables/3cda8ecb-f4c6-4ed4-8506-abe965b54b86", + "storedProcedureCode": { + "langauge": "SQL", + "code": "CREATE OR REPLACE PROCEDURE output_message(message VARCHAR)\nRETURNS VARCHAR NOT NULL\nLANGUAGE SQL\nAS\n$$\nBEGIN\n RETURN message;\nEND;\n$$\n;" + }, + "database": { + "id": "50da1ff8-4e1d-4967-8931-45edbf4dd908", + "type": "database", + "name": "sample_data.ecommerce_db", + "description": "This **mock** database contains tables related to shopify sales and orders with related dimension tables.", + "href": "http://localhost:8585/api/v1/databases/50da1ff8-4e1d-4967-8931-45edbf4dd908" + }, + "tags": [], + "followers": [], + "databaseSchema": { + "id": "d7be1e2c-b3dc-11ec-b909-0242ac120002", + "type": "databaseSchema", + "name": "sample_data.ecommerce_db.shopify", + "description": "This **mock** Schema contains tables related to shopify sales and orders with related dimension tables.", + "href": "http://localhost:8585/api/v1/databaseSchemas/d7be1e2c-b3dc-11ec-b909-0242ac120002" + } + }, + { + "name": "update_orders_table", + "description": "This stored procedure is written java script to update the orders table", + "version": 0.1, + "updatedAt": 1638354087391, + "updatedBy": "anonymous", + "href": "http://localhost:8585/api/v1/tables/3cda8ecb-f4c6-4ed4-8506-abe965b54b86", + "storedProcedureCode": { + "langauge": "JavaScript", + "code": "create or replace procedure read_result_set()\n returns float not null\n language javascript\n as \n $$ \n var my_sql_command = \"select * from table1\";\n var statement1 = snowflake.createStatement( {sqlText: my_sql_command} );\n var result_set1 = statement1.execute();\n // Loop through the results, processing one row at a time... \n while (result_set1.next()) {\n var column1 = result_set1.getColumnValue(1);\n var column2 = result_set1.getColumnValue(2);\n // Do something with the retrieved values...\n }\n return 0.0; // Replace with something more useful.\n $$\n ;" + }, + "database": { + "id": "50da1ff8-4e1d-4967-8931-45edbf4dd908", + "type": "database", + "name": "sample_data.ecommerce_db", + "description": "This **mock** database contains tables related to shopify sales and orders with related dimension tables.", + "href": "http://localhost:8585/api/v1/databases/50da1ff8-4e1d-4967-8931-45edbf4dd908" + }, + "tags": [], + "followers": [], + "databaseSchema": { + "id": "d7be1e2c-b3dc-11ec-b909-0242ac120002", + "type": "databaseSchema", + "name": "sample_data.ecommerce_db.shopify", + "description": "This **mock** Schema contains tables related to shopify sales and orders with related dimension tables.", + "href": "http://localhost:8585/api/v1/databaseSchemas/d7be1e2c-b3dc-11ec-b909-0242ac120002" + } + } + ] +} \ No newline at end of file diff --git a/ingestion/src/metadata/ingestion/ometa/ometa_api.py b/ingestion/src/metadata/ingestion/ometa/ometa_api.py index d614629fce24..7b377f3e8fdb 100644 --- a/ingestion/src/metadata/ingestion/ometa/ometa_api.py +++ b/ingestion/src/metadata/ingestion/ometa/ometa_api.py @@ -67,20 +67,6 @@ T = TypeVar("T", bound=BaseModel) C = TypeVar("C", bound=BaseModel) -# Helps us dynamically load the Entity class path in the -# generated module. -MODULE_PATH = { - "policy": "policies", - "service": "services", - "tag": "classification", - "classification": "classification", - "test": "tests", - "user": "teams", - "role": "teams", - "team": "teams", - "workflow": "automations", -} - class MissingEntityTypeException(Exception): """ @@ -195,12 +181,7 @@ def get_module_path(self, entity: Type[T]) -> str: Based on the entity, return the module path it is found inside generated """ - - for key, value in MODULE_PATH.items(): - if key in entity.__name__.lower(): - return value - - return self.data_path + return entity.__module__.split(".")[-2] def get_create_entity_type(self, entity: Type[T]) -> Type[C]: """ @@ -247,8 +228,8 @@ def get_entity_from_create(self, create: Type[C]) -> Type[T]: .replace("testdefinition", "testDefinition") .replace("testcase", "testCase") .replace("searchindex", "searchIndex") + .replace("storedprocedure", "storedProcedure") ) - class_path = ".".join( filter( None, @@ -260,7 +241,6 @@ def get_entity_from_create(self, create: Type[C]) -> Type[T]: ], ) ) - entity_class = getattr( __import__(class_path, globals(), locals(), [class_name]), class_name ) diff --git a/ingestion/src/metadata/ingestion/ometa/routes.py b/ingestion/src/metadata/ingestion/ometa/routes.py index b57b83214914..5db8ec881d9e 100644 --- a/ingestion/src/metadata/ingestion/ometa/routes.py +++ b/ingestion/src/metadata/ingestion/ometa/routes.py @@ -41,6 +41,9 @@ from metadata.generated.schema.api.data.createSearchIndex import ( CreateSearchIndexRequest, ) +from metadata.generated.schema.api.data.createStoredProcedure import ( + CreateStoredProcedureRequest, +) from metadata.generated.schema.api.data.createTable import CreateTableRequest from metadata.generated.schema.api.data.createTopic import CreateTopicRequest from metadata.generated.schema.api.lineage.addLineage import AddLineageRequest @@ -98,6 +101,7 @@ from metadata.generated.schema.entity.data.query import Query from metadata.generated.schema.entity.data.report import Report from metadata.generated.schema.entity.data.searchIndex import SearchIndex +from metadata.generated.schema.entity.data.storedProcedure import StoredProcedure from metadata.generated.schema.entity.data.table import Table from metadata.generated.schema.entity.data.topic import Topic from metadata.generated.schema.entity.policies.policy import Policy @@ -150,6 +154,8 @@ CreateContainerRequest.__name__: "/containers", SearchIndex.__name__: "/searchIndexes", CreateSearchIndexRequest.__name__: "/searchIndexes", + StoredProcedure.__name__: "/storedProcedures", + CreateStoredProcedureRequest.__name__: "/storedProcedures", # Classifications Tag.__name__: "/tags", CreateTagRequest.__name__: "/tags", diff --git a/ingestion/src/metadata/ingestion/source/database/sample_data.py b/ingestion/src/metadata/ingestion/source/database/sample_data.py index 479289e9c156..e70ecfdfec4a 100644 --- a/ingestion/src/metadata/ingestion/source/database/sample_data.py +++ b/ingestion/src/metadata/ingestion/source/database/sample_data.py @@ -37,6 +37,9 @@ from metadata.generated.schema.api.data.createSearchIndex import ( CreateSearchIndexRequest, ) +from metadata.generated.schema.api.data.createStoredProcedure import ( + CreateStoredProcedureRequest, +) from metadata.generated.schema.api.data.createTable import CreateTableRequest from metadata.generated.schema.api.data.createTableProfile import ( CreateTableProfileRequest, @@ -60,6 +63,7 @@ MlStore, ) from metadata.generated.schema.entity.data.pipeline import Pipeline, PipelineStatus +from metadata.generated.schema.entity.data.storedProcedure import StoredProcedureCode from metadata.generated.schema.entity.data.table import ( ColumnProfile, SystemProfile, @@ -244,6 +248,13 @@ def __init__(self, config: WorkflowSource, metadata_config: OpenMetadataConnecti encoding=UTF_8, ) ) + self.stored_procedures = json.load( + open( # pylint: disable=consider-using-with + sample_data_folder + "/datasets/stored_procedures.json", + "r", + encoding=UTF_8, + ) + ) self.database_service_json = json.load( open( # pylint: disable=consider-using-with sample_data_folder + "/datasets/service.json", @@ -509,6 +520,7 @@ def next_record(self) -> Iterable[Entity]: yield from self.ingest_users() yield from self.ingest_glue() yield from self.ingest_tables() + yield from self.ingest_stored_procedures() yield from self.ingest_topics() yield from self.ingest_charts() yield from self.ingest_data_models() @@ -687,6 +699,65 @@ def ingest_tables(self): ), ) + def ingest_stored_procedures(self): + """ + Ingest Sample Stored Procedures + """ + + db = CreateDatabaseRequest( + name=self.database["name"], + description=self.database["description"], + service=self.database_service.fullyQualifiedName.__root__, + ) + yield db + + database_entity = fqn.build( + self.metadata, + entity_type=Database, + service_name=self.database_service.name.__root__, + database_name=db.name.__root__, + ) + + database_object = self.metadata.get_by_name( + entity=Database, fqn=database_entity + ) + + schema = CreateDatabaseSchemaRequest( + name=self.database_schema["name"], + description=self.database_schema["description"], + database=database_object.fullyQualifiedName, + ) + yield schema + + database_schema_entity = fqn.build( + self.metadata, + entity_type=DatabaseSchema, + service_name=self.database_service.name.__root__, + database_name=db.name.__root__, + schema_name=schema.name.__root__, + ) + + database_schema_object = self.metadata.get_by_name( + entity=DatabaseSchema, fqn=database_schema_entity + ) + + resp = self.metadata.list_entities(entity=User, limit=5) + self.user_entity = resp.entities + + for stored_procedure in self.stored_procedures["storedProcedures"]: + stored_procedure = CreateStoredProcedureRequest( + name=stored_procedure["name"], + description=stored_procedure["description"], + storedProcedureCode=StoredProcedureCode( + **stored_procedure["storedProcedureCode"] + ), + databaseSchema=database_schema_object.fullyQualifiedName, + tags=stored_procedure["tags"], + ) + + self.status.scanned(f"StoredProcedure Scanned: {stored_procedure.name}") + yield stored_procedure + def ingest_topics(self) -> Iterable[CreateTopicRequest]: """ Ingest Sample Topics diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java index 3f973f607d2f..564f631e48da 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/Entity.java @@ -95,6 +95,7 @@ public final class Entity { // Data asset entities // public static final String TABLE = "table"; + public static final String STORED_PROCEDURE = "storedProcedure"; public static final String DATABASE = "database"; public static final String DATABASE_SCHEMA = "databaseSchema"; public static final String METRICS = "metrics"; diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java index d4f89a982eca..65425cd40f4a 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java @@ -80,6 +80,7 @@ import org.openmetadata.schema.entity.data.Query; import org.openmetadata.schema.entity.data.Report; import org.openmetadata.schema.entity.data.SearchIndex; +import org.openmetadata.schema.entity.data.StoredProcedure; import org.openmetadata.schema.entity.data.Table; import org.openmetadata.schema.entity.data.Topic; import org.openmetadata.schema.entity.domains.DataProduct; @@ -167,6 +168,9 @@ public interface CollectionDAO { @CreateSqlObject TableDAO tableDAO(); + @CreateSqlObject + QueryDAO queryDAO(); + @CreateSqlObject UsageDAO usageDAO(); @@ -249,7 +253,7 @@ public interface CollectionDAO { FeedDAO feedDAO(); @CreateSqlObject - QueryDAO queryDAO(); + StoredProcedureDAO storedProcedureDAO(); @CreateSqlObject ChangeEventDAO changeEventDAO(); @@ -1809,6 +1813,23 @@ default List listAfter(ListFilter filter, int limit, String after) { } } + interface StoredProcedureDAO extends EntityDAO { + @Override + default String getTableName() { + return "stored_procedure_entity"; + } + + @Override + default Class getEntityClass() { + return StoredProcedure.class; + } + + @Override + default String getNameHashColumn() { + return "fqnHash"; + } + } + interface QueryDAO extends EntityDAO { @Override default String getTableName() { diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SearchIndexRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SearchIndexRepository.java index 4377f5af66a1..df6b09691c56 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SearchIndexRepository.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/SearchIndexRepository.java @@ -54,6 +54,12 @@ import org.openmetadata.service.util.JsonUtils; public class SearchIndexRepository extends EntityRepository { + + public SearchIndexRepository(CollectionDAO dao) { + super( + SearchIndexResource.COLLECTION_PATH, Entity.SEARCH_INDEX, SearchIndex.class, dao.searchIndexDAO(), dao, "", ""); + } + @Override public void setFullyQualifiedName(SearchIndex searchIndex) { searchIndex.setFullyQualifiedName( @@ -63,11 +69,6 @@ public void setFullyQualifiedName(SearchIndex searchIndex) { } } - public SearchIndexRepository(CollectionDAO dao) { - super( - SearchIndexResource.COLLECTION_PATH, Entity.SEARCH_INDEX, SearchIndex.class, dao.searchIndexDAO(), dao, "", ""); - } - @Override public void prepare(SearchIndex searchIndex) { SearchService searchService = Entity.getEntity(searchIndex.getService(), "", ALL); diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/StoredProcedureRepository.java b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/StoredProcedureRepository.java new file mode 100644 index 000000000000..1620eac65d14 --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/StoredProcedureRepository.java @@ -0,0 +1,117 @@ +package org.openmetadata.service.jdbi3; + +import static org.openmetadata.schema.type.Include.ALL; +import static org.openmetadata.service.Entity.DATABASE_SCHEMA; +import static org.openmetadata.service.Entity.FIELD_FOLLOWERS; +import static org.openmetadata.service.Entity.STORED_PROCEDURE; + +import org.openmetadata.schema.entity.data.DatabaseSchema; +import org.openmetadata.schema.entity.data.StoredProcedure; +import org.openmetadata.schema.type.EntityReference; +import org.openmetadata.schema.type.Relationship; +import org.openmetadata.service.Entity; +import org.openmetadata.service.resources.databases.StoredProcedureResource; +import org.openmetadata.service.util.EntityUtil; +import org.openmetadata.service.util.FullyQualifiedName; + +public class StoredProcedureRepository extends EntityRepository { + static final String PATCH_FIELDS = "storedProcedureCode"; + static final String UPDATE_FIELDS = "storedProcedureCode"; + + public StoredProcedureRepository(CollectionDAO dao) { + super( + StoredProcedureResource.COLLECTION_PATH, + STORED_PROCEDURE, + StoredProcedure.class, + dao.storedProcedureDAO(), + dao, + PATCH_FIELDS, + UPDATE_FIELDS); + } + + @Override + public void setFullyQualifiedName(StoredProcedure storedProcedure) { + storedProcedure.setFullyQualifiedName( + FullyQualifiedName.add(storedProcedure.getDatabaseSchema().getFullyQualifiedName(), storedProcedure.getName())); + } + + @Override + public void prepare(StoredProcedure storedProcedure) { + DatabaseSchema schema = Entity.getEntity(storedProcedure.getDatabaseSchema(), "", ALL); + storedProcedure + .withDatabaseSchema(schema.getEntityReference()) + .withDatabase(schema.getDatabase()) + .withService(schema.getService()) + .withServiceType(schema.getServiceType()); + } + + @Override + public void storeEntity(StoredProcedure storedProcedure, boolean update) { + // Relationships and fields such as service are derived and not stored as part of json + EntityReference service = storedProcedure.getService(); + storedProcedure.withService(null); + store(storedProcedure, update); + storedProcedure.withService(service); + } + + @Override + public void storeRelationships(StoredProcedure storedProcedure) { + addRelationship( + storedProcedure.getDatabaseSchema().getId(), + storedProcedure.getId(), + DATABASE_SCHEMA, + STORED_PROCEDURE, + Relationship.CONTAINS); + } + + @Override + public StoredProcedure setInheritedFields(StoredProcedure storedProcedure, EntityUtil.Fields fields) { + DatabaseSchema schema = + Entity.getEntity(DATABASE_SCHEMA, storedProcedure.getDatabaseSchema().getId(), "owner,domain", ALL); + inheritOwner(storedProcedure, fields, schema); + inheritDomain(storedProcedure, fields, schema); + return storedProcedure; + } + + @Override + public StoredProcedure setFields(StoredProcedure storedProcedure, EntityUtil.Fields fields) { + setDefaultFields(storedProcedure); + storedProcedure.setFollowers(fields.contains(FIELD_FOLLOWERS) ? getFollowers(storedProcedure) : null); + return storedProcedure; + } + + @Override + public StoredProcedure clearFields(StoredProcedure storedProcedure, EntityUtil.Fields fields) { + 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(StoredProcedure storedProcedure, EntityReference service) { + if (service != null && storedProcedure != null) { + addRelationship( + service.getId(), storedProcedure.getId(), service.getType(), STORED_PROCEDURE, Relationship.CONTAINS); + storedProcedure.setService(service); + } + } + + public class StoredProcedureUpdater extends EntityUpdater { + public StoredProcedureUpdater(StoredProcedure original, StoredProcedure updated, Operation operation) { + super(original, updated, operation); + } + + @Override + public void entitySpecificUpdate() { + recordChange("storedProcedureCode", original.getStoredProcedureCode(), updated.getStoredProcedureCode()); + } + } +} diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java new file mode 100644 index 000000000000..78731fe85c5a --- /dev/null +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/databases/StoredProcedureResource.java @@ -0,0 +1,402 @@ +package org.openmetadata.service.resources.databases; + +import io.swagger.v3.oas.annotations.ExternalDocumentation; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.parameters.RequestBody; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.UUID; +import javax.json.JsonPatch; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.ws.rs.*; +import javax.ws.rs.core.*; +import org.openmetadata.schema.api.data.CreateStoredProcedure; +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; +import org.openmetadata.service.jdbi3.CollectionDAO; +import org.openmetadata.service.jdbi3.ListFilter; +import org.openmetadata.service.jdbi3.StoredProcedureRepository; +import org.openmetadata.service.resources.Collection; +import org.openmetadata.service.resources.EntityResource; +import org.openmetadata.service.security.Authorizer; +import org.openmetadata.service.util.ResultList; + +@Path("/v1/storedProcedures") +@Tag( + name = "Stored Procedures", + description = "A `StoredProcedure` entity that contains the set of code statements with an assigned name .") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +@Collection(name = "storedProcedures") +public class StoredProcedureResource extends EntityResource { + public static final String COLLECTION_PATH = "v1/storedProcedures/"; + static final String FIELDS = "owner,tags,followers,extension,domain"; + + @Override + public StoredProcedure addHref(UriInfo uriInfo, StoredProcedure storedProcedure) { + super.addHref(uriInfo, storedProcedure); + Entity.withHref(uriInfo, storedProcedure.getDatabaseSchema()); + Entity.withHref(uriInfo, storedProcedure.getDatabase()); + Entity.withHref(uriInfo, storedProcedure.getService()); + return storedProcedure; + } + + public StoredProcedureResource(CollectionDAO dao, Authorizer authorizer) { + super(StoredProcedure.class, new StoredProcedureRepository(dao), authorizer); + } + + public static class StoredProcedureList extends ResultList { + /* Required for serde */ + } + + @GET + @Operation( + operationId = "listStoredProcedures", + summary = "List Stored Procedures", + description = + "Get a list of stored procedures, optionally filtered by `databaseSchema` it belongs to. Use `fields` " + + "parameter to get only necessary fields. Use cursor-based pagination to limit the number " + + "entries in the list using `limit` and `before` or `after` query params.", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of stored procedures", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedureList.class))) + }) + public ResultList list( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = "Filter stored procedures by database schema", + schema = @Schema(type = "string", example = "customerDatabaseSchema")) + @QueryParam("databaseSchema") + String databaseSchemaParam, + @Parameter(description = "Limit the number schemas returned. (1 to 1000000, default" + " = 10)") + @DefaultValue("10") + @QueryParam("limit") + @Min(0) + @Max(1000000) + int limitParam, + @Parameter(description = "Returns list of schemas before this cursor", schema = @Schema(type = "string")) + @QueryParam("before") + String before, + @Parameter(description = "Returns list of schemas after this cursor", schema = @Schema(type = "string")) + @QueryParam("after") + String after, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) { + ListFilter filter = new ListFilter(include).addQueryParam("databaseSchema", databaseSchemaParam); + return listInternal(uriInfo, securityContext, fieldsParam, filter, limitParam, before, after); + } + + @GET + @Path("/{id}/versions") + @Operation( + operationId = "listAllStoredProceduresVersion", + summary = "List stored procedure versions", + description = "Get a list of all the versions of a stored procedure identified by `Id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "List of schema versions", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = EntityHistory.class))) + }) + public EntityHistory listVersions( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Stored Procedure Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id) { + return super.listVersionsInternal(securityContext, id); + } + + @GET + @Path("/{id}") + @Operation( + operationId = "getStoredProcedureByID", + summary = "Get a stored procedure by Id", + description = "Get a stored procedure by `Id`.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The Stored Procedure", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedure.class))), + @ApiResponse(responseCode = "404", description = "Schema for instance {id} is not found") + }) + public StoredProcedure get( + @Context UriInfo uriInfo, + @Parameter(description = "Stored Procedure Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) { + return getInternal(uriInfo, securityContext, id, fieldsParam, include); + } + + @GET + @Path("/name/{fqn}") + @Operation( + operationId = "getStoredProcedureByFQN", + summary = "Get a Stored Procedure by fully qualified name", + description = "Get a Stored Procedure by fully qualified name.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The schema", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedure.class))), + @ApiResponse(responseCode = "404", description = "Stored Procedure for instance {fqn} is not found") + }) + public StoredProcedure getByName( + @Context UriInfo uriInfo, + @Parameter(description = "Fully qualified name of the Stored Procedure", schema = @Schema(type = "string")) + @PathParam("fqn") + String fqn, + @Context SecurityContext securityContext, + @Parameter( + description = "Fields requested in the returned resource", + schema = @Schema(type = "string", example = FIELDS)) + @QueryParam("fields") + String fieldsParam, + @Parameter( + description = "Include all, deleted, or non-deleted entities.", + schema = @Schema(implementation = Include.class)) + @QueryParam("include") + @DefaultValue("non-deleted") + Include include) { + return getByNameInternal(uriInfo, securityContext, fqn, fieldsParam, include); + } + + @GET + @Path("/{id}/versions/{version}") + @Operation( + operationId = "getSpecificStoredProcedureVersion", + summary = "Get a version of the Stored Procedure", + description = "Get a version of the Stored Procedure by given `Id`", + responses = { + @ApiResponse( + responseCode = "200", + description = "database schema", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedure.class))), + @ApiResponse( + responseCode = "404", + description = "Stored Procedure for instance {id} and version {version} is " + "not found") + }) + public StoredProcedure getVersion( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Stored Procedure Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id, + @Parameter( + description = "Stored Procedure version number in the form `major`.`minor`", + schema = @Schema(type = "string", example = "0.1 or 1.1")) + @PathParam("version") + String version) { + return super.getVersionInternal(securityContext, id, version); + } + + @POST + @Operation( + operationId = "createStoredProcedure", + summary = "Create a Stored Procedure", + description = "Create a Stored Procedure under an existing `service`.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The Stored Procedure", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedure.class))), + @ApiResponse(responseCode = "400", description = "Bad request") + }) + public Response create( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateStoredProcedure create) { + StoredProcedure storedProcedure = getStoredProcedure(create, securityContext.getUserPrincipal().getName()); + return create(uriInfo, securityContext, storedProcedure); + } + + @PATCH + @Path("/{id}") + @Operation( + operationId = "patchStoredProcedure", + summary = "Update a Stored Procedure", + description = "Update an existing StoredProcedure using JsonPatch.", + externalDocs = @ExternalDocumentation(description = "JsonPatch RFC", url = "https://tools.ietf.org/html/rfc6902")) + @Consumes(MediaType.APPLICATION_JSON_PATCH_JSON) + public Response patch( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Stored Procedure Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id, + @RequestBody( + description = "JsonPatch with array of operations", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON_PATCH_JSON, + examples = { + @ExampleObject("[" + "{op:remove, path:/a}," + "{op:add, path: /b, value: val}" + "]") + })) + JsonPatch patch) { + return patchInternal(uriInfo, securityContext, id, patch); + } + + @PUT + @Operation( + operationId = "createOrUpdateStoredProcedure", + summary = "Create or update Stored Procedure", + description = "Create a stored procedure, if it does not exist or update an existing stored procedure.", + responses = { + @ApiResponse( + responseCode = "200", + description = "The updated schema ", + content = + @Content(mediaType = "application/json", schema = @Schema(implementation = StoredProcedure.class))) + }) + public Response createOrUpdate( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateStoredProcedure create) { + StoredProcedure storedProcedure = getStoredProcedure(create, securityContext.getUserPrincipal().getName()); + 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( + operationId = "deleteStoredProcedure", + summary = "Delete a StoredProcedure by Id", + description = "Delete a StoredProcedure by `Id`.", + responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "StoredProcedure for instance {id} is not found") + }) + public Response delete( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Recursively delete this entity and it's children. (Default `false`)") + @DefaultValue("false") + @QueryParam("recursive") + boolean recursive, + @Parameter(description = "Hard delete the entity. (Default = `false`)") + @QueryParam("hardDelete") + @DefaultValue("false") + boolean hardDelete, + @Parameter(description = "Database schema Id", schema = @Schema(type = "UUID")) @PathParam("id") UUID id) { + return delete(uriInfo, securityContext, id, recursive, hardDelete); + } + + @DELETE + @Path("/name/{fqn}") + @Operation( + operationId = "deleteDBSchemaByFQN", + summary = "Delete a schema by fully qualified name", + description = "Delete a schema by `fullyQualifiedName`. Schema can only be deleted if it has no tables.", + responses = { + @ApiResponse(responseCode = "200", description = "OK"), + @ApiResponse(responseCode = "404", description = "Schema for instance {fqn} is not found") + }) + public Response delete( + @Context UriInfo uriInfo, + @Context SecurityContext securityContext, + @Parameter(description = "Hard delete the entity. (Default = `false`)") + @QueryParam("hardDelete") + @DefaultValue("false") + boolean hardDelete, + @Parameter(description = "Name of the DBSchema", schema = @Schema(type = "string")) @PathParam("fqn") + String fqn) { + return deleteByName(uriInfo, securityContext, fqn, false, hardDelete); + } + + @PUT + @Path("/restore") + @Operation( + operationId = "restore", + summary = "Restore a soft deleted database schema.", + description = "Restore a soft deleted database schema.", + responses = { + @ApiResponse( + responseCode = "200", + description = "Successfully restored the DatabaseSchema ", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = DatabaseSchema.class))) + }) + public Response restoreDatabaseSchema( + @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid RestoreEntity restore) { + return restoreEntity(uriInfo, securityContext, restore.getId()); + } + + private StoredProcedure getStoredProcedure(CreateStoredProcedure create, String user) { + return copy(new StoredProcedure(), create, user) + .withDatabaseSchema(getEntityReference(Entity.DATABASE_SCHEMA, create.getDatabaseSchema())) + .withTags(create.getTags()) + .withStoredProcedureCode(create.getStoredProcedureCode()) + .withSourceUrl(create.getSourceUrl()); + } +} diff --git a/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/StoredProcedureResourceTest.java b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/StoredProcedureResourceTest.java new file mode 100644 index 000000000000..1f154cb2a494 --- /dev/null +++ b/openmetadata-service/src/test/java/org/openmetadata/service/resources/databases/StoredProcedureResourceTest.java @@ -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 { + 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 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 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); + } + } +} diff --git a/openmetadata-spec/src/main/resources/json/schema/api/data/createStoredProcedure.json b/openmetadata-spec/src/main/resources/json/schema/api/data/createStoredProcedure.json new file mode 100644 index 000000000000..077a98cfdf1b --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/api/data/createStoredProcedure.json @@ -0,0 +1,60 @@ +{ + "$id": "https://open-metadata.org/schema/api/data/createStoredProcedure.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CreateStoredProcedureRequest", + "description": "Create Stored Procedure Request", + "type": "object", + "javaType": "org.openmetadata.schema.api.data.CreateStoredProcedure", + "javaInterfaces": [ + "org.openmetadata.schema.CreateEntity" + ], + "properties": { + "name": { + "description": "Name of a Stored Procedure.", + "$ref": "../../entity/data/storedProcedure.json#/definitions/entityName" + }, + "displayName": { + "description": "Display Name that identifies this Stored Procedure.", + "type": "string" + }, + "description": { + "description": "Description of the Stored Procedure.", + "$ref": "../../type/basic.json#/definitions/markdown" + }, + "owner": { + "description": "Owner of this entity", + "$ref": "../../type/entityReference.json", + "default": null + }, + "tags": { + "description": "Tags for this StoredProcedure.", + "type": "array", + "items": { + "$ref": "../../type/tagLabel.json" + }, + "default": null + }, + "storedProcedureCode": { + "description": "SQL Query definition.", + "$ref": "../../entity/data/storedProcedure.json#/definitions/storedProcedureCode" + }, + "databaseSchema": { + "description": "Link to the database schema fully qualified name where this stored procedure is hosted in", + "$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName" + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" + }, + "sourceUrl": { + "description": "Source URL of database schema.", + "$ref": "../../type/basic.json#/definitions/sourceUrl" + }, + "domain" : { + "description": "Fully qualified name of the domain the Stored Procedure belongs to.", + "type": "string" + } + }, + "required": ["name", "storedProcedureCode"], + "additionalProperties": false +} \ No newline at end of file diff --git a/openmetadata-spec/src/main/resources/json/schema/entity/data/storedProcedure.json b/openmetadata-spec/src/main/resources/json/schema/entity/data/storedProcedure.json new file mode 100644 index 000000000000..0ac2d0c7583d --- /dev/null +++ b/openmetadata-spec/src/main/resources/json/schema/entity/data/storedProcedure.json @@ -0,0 +1,158 @@ +{ + "$id": "https://open-metadata.org/schema/entity/data/storedProcedure.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StoredProcedure", + "$comment": "@om-entity-type", + "description": "A `StoredProcedure` entity that contains the set of code statements with an assigned name and is defined in a `Database Schema`.\"", + "type": "object", + "javaType": "org.openmetadata.schema.entity.data.StoredProcedure", + "javaInterfaces": ["org.openmetadata.schema.EntityInterface"], + "definitions": { + "entityName": { + "description": "Name of a Stored Procedure. Expected to be unique within a database schema.", + "type": "string", + "minLength": 1, + "maxLength": 256, + "pattern": "^((?!::).)*$" + }, + "storedProcedureCode": { + "properties": { + "language": { + "javaType": "org.openmetadata.schema.type.StoredProcedureLanguage", + "description": "This schema defines the type of the language used for Stored Procedure's Code.", + "type": "string", + "enum": [ + "SQL", + "Java", + "JavaScript", + "Python" + ], + "javaEnums": [ + { + "name": "SQL" + }, + { + "name": "Java" + }, + { + "name": "JavaScript" + }, + { + "name": "Python" + } + ] + }, + "code": { + "javaType": "org.openmetadata.schema.type.StoredProcedureCode", + "description": "This schema defines the type of the language used for Stored Procedure's Code.", + "type": "string" + } + } + } + }, + "properties": { + "id": { + "description": "Unique identifier of the StoredProcedure.", + "$ref": "../../type/basic.json#/definitions/uuid" + }, + "name": { + "description": "Name of Stored Procedure.", + "$ref": "#/definitions/entityName" + }, + "fullyQualifiedName": { + "description": "Fully qualified name of a Stored Procedure.", + "$ref": "../../type/basic.json#/definitions/fullyQualifiedEntityName" + }, + "displayName": { + "description": "Display Name that identifies this Stored Procedure.", + "type": "string" + }, + "description": { + "description": "Description of a Stored Procedure.", + "$ref": "../../type/basic.json#/definitions/markdown" + }, + "storedProcedureCode": { + "description": "Stored Procedure Code.", + "$ref": "#/definitions/storedProcedureCode" + }, + "version": { + "description": "Metadata version of the Stored Procedure.", + "$ref": "../../type/entityHistory.json#/definitions/entityVersion" + }, + "updatedAt": { + "description": "Last update time corresponding to the new version of the entity in Unix epoch time milliseconds.", + "$ref": "../../type/basic.json#/definitions/timestamp" + }, + "updatedBy": { + "description": "User who made the query.", + "type": "string" + }, + "href": { + "description": "Link to this Query resource.", + "$ref": "../../type/basic.json#/definitions/href" + }, + "changeDescription": { + "description": "Change that lead to this version of the entity.", + "$ref": "../../type/entityHistory.json#/definitions/changeDescription" + }, + "databaseSchema": { + "description": "Reference to Database Schema that contains this stored procedure.", + "$ref": "../../type/entityReference.json" + }, + "database": { + "description": "Reference to Database that contains this stored procedure.", + "$ref": "../../type/entityReference.json" + }, + "service": { + "description": "Link to Database service this table is hosted in.", + "$ref": "../../type/entityReference.json" + }, + "serviceType": { + "description": "Service type this table is hosted in.", + "$ref": "../services/databaseService.json#/definitions/databaseServiceType" + }, + "deleted": { + "description": "When `true` indicates the entity has been soft deleted.", + "type": "boolean", + "default": false + }, + "owner": { + "description": "Owner of this Query.", + "$ref": "../../type/entityReference.json", + "default": null + }, + "followers": { + "description": "Followers of this Query.", + "$ref": "../../type/entityReferenceList.json" + }, + "votes" : { + "$ref": "../../type/votes.json" + }, + "code": { + "description": "SQL Query definition.", + "$ref": "../../type/basic.json#/definitions/sqlQuery" + }, + "tags": { + "description": "Tags for this SQL query.", + "type": "array", + "items": { + "$ref": "../../type/tagLabel.json" + }, + "default": null + }, + "extension": { + "description": "Entity extension data with custom attributes added to the entity.", + "$ref": "../../type/basic.json#/definitions/entityExtension" + }, + "sourceUrl": { + "description": "Source URL of database schema.", + "$ref": "../../type/basic.json#/definitions/sourceUrl" + }, + "domain" : { + "description": "Domain the Stored Procedure belongs to. When not set, the Stored Procedure inherits the domain from the database schemna it belongs to.", + "$ref": "../../type/entityReference.json" + } + }, + "required": ["name","storedProcedureCode"], + "additionalProperties": false +}