saola_psl_core/configuration/
datasource.rs1use schema_ast::ast::WithSpan;
2use serde::Deserialize;
3
4use crate::{
5 configuration::StringFromEnvVar,
6 datamodel_connector::{Connector, ConnectorCapabilities, RelationMode},
7 diagnostics::{DatamodelError, Diagnostics, Span},
8 set_config_dir,
9};
10use std::{any::Any, borrow::Cow, path::Path, sync::Arc};
11
12#[derive(Debug, Clone, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct DatasourceUrls {
15 pub url: String,
16 pub shadow_database_url: Option<String>,
17 pub direct_url: Option<String>,
18}
19
20#[derive(Clone)]
22pub struct Datasource {
23 pub name: String,
24 pub span: Span,
26 pub provider: String,
28 pub active_provider: &'static str,
30 pub documentation: Option<String>,
31 pub active_connector: &'static dyn Connector,
33 pub relation_mode: Option<RelationMode>,
35 pub namespaces: Vec<(String, Span)>,
37 pub schemas_span: Option<Span>,
38 pub connector_data: DatasourceConnectorData,
39
40 pub url: StringFromEnvVar,
43 pub url_span: Span,
44 pub direct_url: Option<StringFromEnvVar>,
45 pub direct_url_span: Option<Span>,
46 pub shadow_database_url: Option<(StringFromEnvVar, Span)>,
48}
49
50pub enum UrlValidationError {
51 EmptyUrlValue,
52 EmptyEnvValue(String),
53 NoEnvValue(String),
54 NoUrlOrEnv,
55}
56
57#[derive(Clone, Default)]
58pub struct DatasourceConnectorData {
59 data: Option<Arc<dyn Any + Send + Sync + 'static>>,
60}
61
62impl DatasourceConnectorData {
63 pub fn new(data: Arc<dyn Any + Send + Sync + 'static>) -> Self {
64 Self { data: Some(data) }
65 }
66
67 #[track_caller]
68 pub fn downcast_ref<T: 'static>(&self) -> Option<&T> {
69 self.data.as_ref().map(|data| data.downcast_ref().unwrap())
70 }
71}
72
73impl std::fmt::Debug for Datasource {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 f.debug_struct("Datasource")
76 .field("name", &self.name)
77 .field("provider", &self.provider)
78 .field("active_provider", &self.active_provider)
79 .field("url", &"<url>")
80 .field("documentation", &self.documentation)
81 .field("active_connector", &&"...")
82 .field("shadow_database_url", &"<shadow_database_url>")
83 .field("relation_mode", &self.relation_mode)
84 .field("namespaces", &self.namespaces)
85 .finish()
86 }
87}
88
89impl Datasource {
90 pub fn override_urls(&mut self, datasource_urls_override: DatasourceUrls) {
91 self.url = StringFromEnvVar {
92 value: Some(datasource_urls_override.url),
93 from_env_var: None,
94 };
95 self.direct_url = datasource_urls_override.direct_url.map(|url| StringFromEnvVar {
96 value: Some(url),
97 from_env_var: None,
98 });
99 self.shadow_database_url = datasource_urls_override.shadow_database_url.map(|url| {
100 (
101 StringFromEnvVar {
102 value: Some(url),
103 from_env_var: None,
104 },
105 self.url_span,
106 )
107 });
108 }
109
110 #[track_caller]
112 pub fn downcast_connector_data<T: 'static>(&self) -> Option<&T> {
113 self.connector_data.downcast_ref()
114 }
115
116 pub(crate) fn has_schema(&self, name: &str) -> bool {
117 self.namespaces.binary_search_by_key(&name, |(s, _)| s).is_ok()
118 }
119
120 pub fn capabilities(&self) -> ConnectorCapabilities {
121 self.active_connector.capabilities()
122 }
123
124 #[allow(clippy::or_fun_call)] pub fn relation_mode(&self) -> RelationMode {
127 self.relation_mode
128 .unwrap_or(self.active_connector.default_relation_mode())
129 }
130
131 pub fn load_url<F>(&self, env: F) -> Result<String, Diagnostics>
134 where
135 F: Fn(&str) -> Option<String>,
136 {
137 let url = self.load_url_no_validation(env)?;
138
139 self.active_connector.validate_url(&url).map_err(|err_str| {
140 let err_str = if url.starts_with("prisma") {
141 let s = indoc::formatdoc! {"
142 {err_str}
143
144 To use a URL with protocol `prisma://`, you need to either enable Accelerate or the Data Proxy.
145 Enable Accelerate via `prisma generate --accelerate` or the Data Proxy via `prisma generate --data-proxy.`
146
147 More information about Data Proxy: https://pris.ly/d/data-proxy
148 "};
149
150 Cow::from(s)
151 } else {
152 Cow::from(err_str)
153 };
154
155 DatamodelError::new_source_validation_error(&format!("the URL {}", &err_str), &self.name, self.url_span)
156 })?;
157
158 Ok(url)
159 }
160
161 pub fn load_url_no_validation<F>(&self, env: F) -> Result<String, Diagnostics>
164 where
165 F: Fn(&str) -> Option<String>,
166 {
167 from_url(&self.url, env).map_err(|err| match err {
168 UrlValidationError::EmptyUrlValue => {
169 let msg = "You must provide a nonempty URL";
170 DatamodelError::new_source_validation_error(msg, &self.name, self.url_span).into()
171 }
172 UrlValidationError::EmptyEnvValue(env_var) => DatamodelError::new_source_validation_error(
173 &format!(
174 "You must provide a nonempty URL. The environment variable `{env_var}` resolved to an empty string."
175 ),
176 &self.name,
177 self.url_span,
178 )
179 .into(),
180 UrlValidationError::NoEnvValue(env_var) => {
181 DatamodelError::new_environment_functional_evaluation_error(env_var, self.url_span).into()
182 }
183 UrlValidationError::NoUrlOrEnv => unreachable!("Missing url in datasource"),
184 })
185 }
186
187 pub fn load_direct_url<F>(&self, env: F) -> Result<String, Diagnostics>
191 where
192 F: Fn(&str) -> Option<String>,
193 {
194 let validate_direct_url = |(url, span)| {
195 let handle_err = |err| match err {
196 UrlValidationError::EmptyUrlValue => {
197 let msg = "You must provide a nonempty direct URL";
198 Err(DatamodelError::new_source_validation_error(msg, &self.name, span).into())
199 }
200 UrlValidationError::EmptyEnvValue(env_var) => {
201 let msg = format!(
202 "You must provide a nonempty direct URL. The environment variable `{env_var}` resolved to an empty string."
203 );
204
205 Err(DatamodelError::new_source_validation_error(&msg, &self.name, span).into())
206 }
207 UrlValidationError::NoEnvValue(env_var) => {
208 let e = DatamodelError::new_environment_functional_evaluation_error(env_var, span);
209 Err(e.into())
210 }
211 UrlValidationError::NoUrlOrEnv => self.load_url(&env),
212 };
213
214 let url = from_url(&url, &env).map_or_else(handle_err, Result::Ok)?;
215
216 if url.starts_with("prisma://") {
217 let msg = "You must provide a direct URL that points directly to the database. Using `prisma` in URL scheme is not allowed.";
218 let e = DatamodelError::new_source_validation_error(msg, &self.name, span);
219
220 Err(e.into())
221 } else {
222 Ok(url)
223 }
224 };
225
226 self.direct_url
227 .clone()
228 .and_then(|url| self.direct_url_span.map(|span| (url, span)))
229 .map_or_else(|| self.load_url(&env), validate_direct_url)
230 }
231
232 pub fn load_url_with_config_dir<F>(&self, config_dir: &Path, env: F) -> Result<String, Diagnostics>
244 where
245 F: Fn(&str) -> Option<String>,
246 {
247 let url = self.load_url(env)?;
249 let url = set_config_dir(self.active_connector.flavour(), config_dir, &url);
250
251 Ok(url.into_owned())
252 }
253
254 pub fn load_shadow_database_url(&self) -> Result<Option<String>, Diagnostics> {
256 let (url, url_span) = match self
257 .shadow_database_url
258 .as_ref()
259 .map(|(url, span)| (&url.value, &url.from_env_var, span))
260 {
261 None => return Ok(None),
262 Some((Some(lit), _, span)) => (lit.clone(), span),
263 Some((None, Some(env_var), span)) => match std::env::var(env_var) {
264 Ok(var) if var.trim().is_empty() => return Ok(None),
266 Err(_) => return Ok(None),
267
268 Ok(var) => (var, span),
269 },
270 Some((None, None, _span)) => unreachable!("Missing url in datasource"),
271 };
272
273 if !url.trim().is_empty() {
274 self.active_connector.validate_url(&url).map_err(|err_str| {
275 DatamodelError::new_source_validation_error(
276 &format!("the shadow database URL {}", &err_str),
277 &self.name,
278 *url_span,
279 )
280 })?;
281 }
282
283 Ok(Some(url))
284 }
285
286 pub fn provider_defined(&self) -> bool {
288 !self.provider.is_empty()
289 }
290
291 pub fn url_defined(&self) -> bool {
292 self.url_span.end > self.url_span.start
293 }
294
295 pub fn direct_url_defined(&self) -> bool {
296 self.direct_url.is_some()
297 }
298
299 pub fn shadow_url_defined(&self) -> bool {
300 self.shadow_database_url.is_some()
301 }
302
303 pub fn relation_mode_defined(&self) -> bool {
304 self.relation_mode.is_some()
305 }
306
307 pub fn schemas_defined(&self) -> bool {
308 self.schemas_span.is_some()
309 }
310}
311
312impl WithSpan for Datasource {
313 fn span(&self) -> Span {
314 self.span
315 }
316}
317
318pub(crate) fn from_url<F>(url: &StringFromEnvVar, env: F) -> Result<String, UrlValidationError>
319where
320 F: Fn(&str) -> Option<String>,
321{
322 let url = match (&url.value, &url.from_env_var) {
323 (Some(lit), _) if lit.trim().is_empty() => {
324 return Err(UrlValidationError::EmptyUrlValue);
325 }
326 (Some(lit), _) => lit.clone(),
327 (None, Some(env_var)) => match env(env_var) {
328 Some(var) if var.trim().is_empty() => {
329 return Err(UrlValidationError::EmptyEnvValue(env_var.clone()));
330 }
331 Some(var) => var,
332 None => return Err(UrlValidationError::NoEnvValue(env_var.clone())),
333 },
334 (None, None) => return Err(UrlValidationError::NoUrlOrEnv),
335 };
336
337 Ok(url)
338}