Skip to main content

unitycatalog_server/api/
registered_models.rs

1use itertools::Itertools;
2
3use unitycatalog_common::models::ObjectLabel;
4use unitycatalog_common::models::registered_models::v1::*;
5use unitycatalog_common::models::{ResourceIdent, ResourceName};
6
7use super::staging_tables::{child_location, resolve_managed_parent_location};
8use super::{RequestContext, SecuredAction};
9pub use crate::codegen::registered_models::RegisteredModelHandler;
10use crate::policy::{Permission, Policy, process_resources};
11use crate::services::{ProvidesLocalStoragePolicy, ProvidesManagedStorageRoot};
12use crate::store::ResourceStore;
13use crate::{Error, Result};
14
15#[async_trait::async_trait]
16impl<
17    T: ResourceStore
18        + Policy<RequestContext>
19        + ProvidesLocalStoragePolicy
20        + ProvidesManagedStorageRoot,
21> RegisteredModelHandler<RequestContext> for T
22{
23    #[tracing::instrument(skip(self, context), fields(resource_name))]
24    async fn list_registered_models(
25        &self,
26        request: ListRegisteredModelsRequest,
27        context: RequestContext,
28    ) -> Result<ListRegisteredModelsResponse> {
29        self.check_required(&request, &context).await?;
30        // `catalog_name`/`schema_name` are optional filters. When both are set we
31        // scope the listing to that schema; otherwise we list across the broader
32        // namespace (matching the UC OSS "list all models" semantics).
33        let namespace = match (&request.catalog_name, &request.schema_name) {
34            (Some(catalog), Some(schema)) => Some(ResourceName::new([catalog, schema])),
35            (Some(catalog), None) => Some(ResourceName::new([catalog])),
36            _ => None,
37        };
38        let (mut resources, next_page_token) = self
39            .list(
40                &ObjectLabel::RegisteredModel,
41                namespace.as_ref(),
42                request.max_results.map(|v| v as usize),
43                request.page_token,
44            )
45            .await?;
46        process_resources(self, &context, &Permission::Read, &mut resources).await?;
47        Ok(ListRegisteredModelsResponse {
48            registered_models: resources.into_iter().map(|r| r.try_into()).try_collect()?,
49            next_page_token,
50            ..Default::default()
51        })
52    }
53
54    #[tracing::instrument(skip(self, context), fields(resource_name))]
55    async fn create_registered_model(
56        &self,
57        request: CreateRegisteredModelRequest,
58        context: RequestContext,
59    ) -> Result<RegisteredModel> {
60        self.check_required(&request, &context).await?;
61        tracing::Span::current().record("resource_name", &request.name);
62
63        // A registered model owns a managed storage location under which its model
64        // versions' artifacts are stored. Derive it from the managed parent location
65        // resolved for the schema/catalog (schema → catalog → metastore), mirroring
66        // managed volumes/tables. The id is allocated here so the path segment equals
67        // the model's id and survives renames.
68        let parent =
69            resolve_managed_parent_location(self, &request.catalog_name, &request.schema_name)
70                .await?;
71        let id = uuid::Uuid::now_v7().hyphenated().to_string();
72        let storage_location = child_location(&parent, "models", &id);
73
74        let full_name = format!(
75            "{}.{}.{}",
76            request.catalog_name, request.schema_name, request.name
77        );
78        let resource = RegisteredModel {
79            name: request.name,
80            catalog_name: request.catalog_name,
81            schema_name: request.schema_name,
82            full_name,
83            storage_location: Some(storage_location),
84            comment: request.comment,
85            id: Some(id),
86            ..Default::default()
87        };
88        Ok(self.create(resource.into()).await?.0.try_into()?)
89    }
90
91    #[tracing::instrument(skip(self, context), fields(resource_name))]
92    async fn get_registered_model(
93        &self,
94        request: GetRegisteredModelRequest,
95        context: RequestContext,
96    ) -> Result<RegisteredModel> {
97        tracing::Span::current().record("resource_name", &request.full_name);
98        self.check_required(&request, &context).await?;
99        Ok(self.get(&request.resource()).await?.0.try_into()?)
100    }
101
102    #[tracing::instrument(skip(self, context), fields(resource_name))]
103    async fn update_registered_model(
104        &self,
105        request: UpdateRegisteredModelRequest,
106        context: RequestContext,
107    ) -> Result<RegisteredModel> {
108        tracing::Span::current().record("resource_name", &request.full_name);
109        self.check_required(&request, &context).await?;
110        let ident = request.resource();
111        let name = ResourceName::from_naive_str_split(request.full_name.as_str());
112        let [catalog_name, schema_name, model_name] = name.as_ref() else {
113            return Err(Error::invalid_argument(
114                "Invalid model name - expected <catalog_name>.<schema_name>.<model_name>",
115            ));
116        };
117        let new_name = request.new_name.as_deref().unwrap_or(model_name);
118        let resource = RegisteredModel {
119            name: new_name.to_owned(),
120            catalog_name: catalog_name.to_owned(),
121            schema_name: schema_name.to_owned(),
122            full_name: format!("{}.{}.{}", catalog_name, schema_name, new_name),
123            comment: request.comment,
124            owner: request.owner,
125            ..Default::default()
126        };
127        Ok(self.update(&ident, resource.into()).await?.0.try_into()?)
128    }
129
130    #[tracing::instrument(skip(self, context), fields(resource_name))]
131    async fn delete_registered_model(
132        &self,
133        request: DeleteRegisteredModelRequest,
134        context: RequestContext,
135    ) -> Result<()> {
136        tracing::Span::current().record("resource_name", &request.full_name);
137        self.check_required(&request, &context).await?;
138        Ok(self.delete(&request.resource()).await?)
139    }
140}
141
142impl SecuredAction for CreateRegisteredModelRequest {
143    fn resource(&self) -> ResourceIdent {
144        ResourceIdent::registered_model(ResourceName::new([
145            self.catalog_name.as_str(),
146            self.schema_name.as_str(),
147            self.name.as_str(),
148        ]))
149    }
150
151    fn permission(&self) -> &'static Permission {
152        &Permission::Create
153    }
154}
155
156impl SecuredAction for ListRegisteredModelsRequest {
157    fn resource(&self) -> ResourceIdent {
158        ResourceIdent::registered_model(ResourceName::new(
159            [self.catalog_name.as_deref(), self.schema_name.as_deref()]
160                .into_iter()
161                .flatten(),
162        ))
163    }
164
165    fn permission(&self) -> &'static Permission {
166        &Permission::Read
167    }
168}
169
170impl SecuredAction for GetRegisteredModelRequest {
171    fn resource(&self) -> ResourceIdent {
172        ResourceIdent::registered_model(ResourceName::from_naive_str_split(self.full_name.as_str()))
173    }
174
175    fn permission(&self) -> &'static Permission {
176        &Permission::Read
177    }
178}
179
180impl SecuredAction for UpdateRegisteredModelRequest {
181    fn resource(&self) -> ResourceIdent {
182        ResourceIdent::registered_model(ResourceName::from_naive_str_split(self.full_name.as_str()))
183    }
184
185    fn permission(&self) -> &'static Permission {
186        &Permission::Manage
187    }
188}
189
190impl SecuredAction for DeleteRegisteredModelRequest {
191    fn resource(&self) -> ResourceIdent {
192        ResourceIdent::registered_model(ResourceName::from_naive_str_split(self.full_name.as_str()))
193    }
194
195    fn permission(&self) -> &'static Permission {
196        &Permission::Manage
197    }
198}