Skip to content

Commit 87bc6b8

Browse files
Serhii Tatarintsevtomhoulejkomyno
authored
WIP(schema-wasm): support schema split into multiple files (#4787)
* Implement multi-file schema handling in PSL This commit implements multi-file schema handling in the Prisma Schema Language. At a high level, instead of accepting a single string, `psl::validate_multi_file()` is an alternative to `psl::validate()` that accepts something morally equivalent to: ```json { "./prisma/schema/a.prisma": "datasource db { ... }", "./prisma/schema/nested/b.prisma": "model Test { ... }" } ``` There are tests for PSL validation with multiple schema files, but most of the rest of engines still consumes the single file version of `psl::validate()`. The implementation and the return type are shared between `psl::validate_multi_file()` and `psl::validate()`, so the change is completely transparent, other than the expectation of passing in a list of (file_name, file_contents) instead of a single string. The `psl::validate()` entry point should behave exactly the same as `psl::multi_schema()` with a single file named `schema.prisma`. In particular, it has the exact same return type. Implementation ============== This is achieved by extending `Span` to contain, in addition to a start and end offset, a `FileId`. The `FileId` is a unique identifier for a file and its parsed `SchemaAst` inside `ParserDatabase`. The identifier types for AST items in `ParserDatabase` are also extended to contain the `FileId`, so that they can be uniquely referred to in the context of the (multi-file) schema. After the analysis phase (the `parser_database` crate), consumers of the analyzed schema become multi-file aware completely transparently, no change is necessary in the other engines. The only changes that will be required at scattered points across the codebase are the `psl::validate()` call sites that will need to receive a `Vec<Box<Path>, SourceFile>` instead of a single `SourceFile`. This PR does _not_ deal with that, but it makes where these call sites are obvious by what entry points they use: `psl::validate()`, `psl::parse_schema()` and the various `*_assert_single()` methods on `ParserDatabase`. The PR contains tests confirming that schema analysis, validation and displaying diagnostics across multiple files works as expected. Status of this PR ================= This is going to be directly mergeable after review, and it will not affect the current schema handling behaviour when dealing with a single schema file. Next steps ========== - Replace all calls to `psl::validate()` with calls to `psl::validate_multi_file()`. - The `*_assert_single()` calls should be progressively replaced with their multi-file counterparts across engines. - The language server should start sending multiple files to prisma-schema-wasm in all calls. This is not in the spirit of the language server spec, but that is the most immediate solution. We'll have to make `range_to_span()` in `prisma-fmt` multi-schema aware by taking a FileId param. Links ===== Relevant issue: prisma/prisma#2377 Also see the [internal design doc](https://www.notion.so/prismaio/Multi-file-Schema-24d68fe8664048ad86252fe446caac24?d=68ef128f25974e619671a9855f65f44d#2889a038e68c4fe1ac9afe3cd34978bd). * WIP(schema-wasm): Support schema split into multiple files * Reformat support (psl crate) * Add multifile reformatting tests * Clippy * feat(prisma-fmt): addd support for mergeSchemas, expose functions to prisma-fmt-wasm * chore(prisma-fmt): removed unused function * chore: fix typo Co-authored-by: Serhii Tatarintsev <[email protected]> * feat(prisma-fmt): apply validation to merge_schemas * chore(prisma-fmt): update unit test * chore: fix bad merge * chore: fix tests --------- Co-authored-by: Tom Houlé <[email protected]> Co-authored-by: Alberto Schiabel <[email protected]> Co-authored-by: jkomyno <[email protected]>
1 parent dcdb692 commit 87bc6b8

File tree

43 files changed

+750
-86
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+750
-86
lines changed

prisma-fmt/src/code_actions/multi_schema.rs

+1-2
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,7 @@ pub(super) fn add_schema_to_schemas(
147147
)
148148
}
149149
None => {
150-
let has_properties = datasource.provider_defined()
151-
|| datasource.url_defined()
150+
let has_properties = datasource.provider_defined() | datasource.url_defined()
152151
|| datasource.direct_url_defined()
153152
|| datasource.shadow_url_defined()
154153
|| datasource.relation_mode_defined()

prisma-fmt/src/get_config.rs

+29-20
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
use psl::Diagnostics;
1+
use psl::{Diagnostics, ValidatedSchema};
22
use serde::Deserialize;
33
use serde_json::json;
44
use std::collections::HashMap;
55

6-
use crate::validate::SCHEMA_PARSER_ERROR_CODE;
6+
use crate::{schema_file_input::SchemaFileInput, validate::SCHEMA_PARSER_ERROR_CODE};
77

88
#[derive(Deserialize, Debug)]
99
#[serde(rename_all = "camelCase")]
1010
struct GetConfigParams {
11-
prisma_schema: String,
11+
prisma_schema: SchemaFileInput,
1212
#[serde(default)]
1313
ignore_env_var_errors: bool,
1414
#[serde(default)]
@@ -43,29 +43,38 @@ pub(crate) fn get_config(params: &str) -> Result<String, String> {
4343
}
4444

4545
fn get_config_impl(params: GetConfigParams) -> Result<serde_json::Value, GetConfigError> {
46-
let wrap_get_config_err = |errors: Diagnostics| -> GetConfigError {
47-
use std::fmt::Write as _;
48-
49-
let mut full_error = errors.to_pretty_string("schema.prisma", &params.prisma_schema);
50-
write!(full_error, "\nValidation Error Count: {}", errors.errors().len()).unwrap();
51-
52-
GetConfigError {
53-
// this mirrors user_facing_errors::common::SchemaParserError
54-
error_code: Some(SCHEMA_PARSER_ERROR_CODE),
55-
message: full_error,
56-
}
57-
};
58-
59-
let mut config = psl::parse_configuration(&params.prisma_schema).map_err(wrap_get_config_err)?;
46+
let mut schema = psl::validate_multi_file(params.prisma_schema.into());
47+
if schema.diagnostics.has_errors() {
48+
return Err(create_get_config_error(&schema, &schema.diagnostics));
49+
}
6050

6151
if !params.ignore_env_var_errors {
6252
let overrides: Vec<(_, _)> = params.datasource_overrides.into_iter().collect();
63-
config
53+
schema
54+
.configuration
6455
.resolve_datasource_urls_prisma_fmt(&overrides, |key| params.env.get(key).map(String::from))
65-
.map_err(wrap_get_config_err)?;
56+
.map_err(|diagnostics| create_get_config_error(&schema, &diagnostics))?;
6657
}
6758

68-
Ok(psl::get_config(&config))
59+
Ok(psl::get_config(&schema.configuration))
60+
}
61+
62+
fn create_get_config_error(schema: &ValidatedSchema, diagnostics: &Diagnostics) -> GetConfigError {
63+
use std::fmt::Write as _;
64+
65+
let mut rendered_diagnostics = schema.render_diagnostics(diagnostics);
66+
write!(
67+
rendered_diagnostics,
68+
"\nValidation Error Count: {}",
69+
diagnostics.errors().len()
70+
)
71+
.unwrap();
72+
73+
GetConfigError {
74+
// this mirrors user_facing_errors::common::SchemaParserError
75+
error_code: Some(SCHEMA_PARSER_ERROR_CODE),
76+
message: rendered_diagnostics,
77+
}
6978
}
7079

7180
#[cfg(test)]

prisma-fmt/src/get_dmmf.rs

+44-3
Large diffs are not rendered by default.

prisma-fmt/src/lib.rs

+10
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ mod code_actions;
33
mod get_config;
44
mod get_dmmf;
55
mod lint;
6+
mod merge_schemas;
67
mod native;
78
mod preview;
9+
mod schema_file_input;
810
mod text_document_completion;
911
mod validate;
1012

@@ -89,6 +91,14 @@ pub fn validate(validate_params: String) -> Result<(), String> {
8991
validate::validate(&validate_params)
9092
}
9193

94+
/// Given a list of Prisma schema files (and their locations), returns the merged schema.
95+
/// This is useful for `@prisma/client` generation, where the client needs a single - potentially large - schema,
96+
/// while still allowing the user to split their schema copies into multiple files.
97+
/// Internally, it uses `[validate]`.
98+
pub fn merge_schemas(params: String) -> Result<String, String> {
99+
merge_schemas::merge_schemas(&params)
100+
}
101+
92102
pub fn native_types(schema: String) -> String {
93103
native::run(&schema)
94104
}

prisma-fmt/src/merge_schemas.rs

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
use psl::reformat_validated_schema_into_single;
2+
use serde::Deserialize;
3+
4+
use crate::schema_file_input::SchemaFileInput;
5+
6+
#[derive(Debug, Deserialize)]
7+
pub struct MergeSchemasParams {
8+
schema: SchemaFileInput,
9+
}
10+
11+
pub(crate) fn merge_schemas(params: &str) -> Result<String, String> {
12+
let params: MergeSchemasParams = match serde_json::from_str(params) {
13+
Ok(params) => params,
14+
Err(serde_err) => {
15+
panic!("Failed to deserialize MergeSchemasParams: {serde_err}");
16+
}
17+
};
18+
19+
let validated_schema = crate::validate::run(params.schema, false)?;
20+
21+
let indent_width = 2usize;
22+
let merged_schema = reformat_validated_schema_into_single(validated_schema, indent_width).unwrap();
23+
24+
Ok(merged_schema)
25+
}
26+
27+
#[cfg(test)]
28+
mod tests {
29+
use super::*;
30+
use expect_test::expect;
31+
use serde_json::json;
32+
33+
#[test]
34+
fn merge_two_valid_schemas_succeeds() {
35+
let schema = vec![
36+
(
37+
"b.prisma",
38+
r#"
39+
model B {
40+
id String @id
41+
a A?
42+
}
43+
"#,
44+
),
45+
(
46+
"a.prisma",
47+
r#"
48+
datasource db {
49+
provider = "postgresql"
50+
url = env("DBURL")
51+
}
52+
53+
model A {
54+
id String @id
55+
b_id String @unique
56+
b B @relation(fields: [b_id], references: [id])
57+
}
58+
"#,
59+
),
60+
];
61+
62+
let request = json!({
63+
"schema": schema,
64+
});
65+
66+
let expected = expect![[r#"
67+
model B {
68+
id String @id
69+
a A?
70+
}
71+
72+
datasource db {
73+
provider = "postgresql"
74+
url = env("DBURL")
75+
}
76+
77+
model A {
78+
id String @id
79+
b_id String @unique
80+
b B @relation(fields: [b_id], references: [id])
81+
}
82+
"#]];
83+
84+
let response = merge_schemas(&request.to_string()).unwrap();
85+
expected.assert_eq(&response);
86+
}
87+
88+
#[test]
89+
fn merge_two_invalid_schemas_panics() {
90+
let schema = vec![
91+
(
92+
"b.prisma",
93+
r#"
94+
model B {
95+
id String @id
96+
a A?
97+
}
98+
"#,
99+
),
100+
(
101+
"a.prisma",
102+
r#"
103+
datasource db {
104+
provider = "postgresql"
105+
url = env("DBURL")
106+
}
107+
108+
model A {
109+
id String @id
110+
b_id String @unique
111+
}
112+
"#,
113+
),
114+
];
115+
116+
let request = json!({
117+
"schema": schema,
118+
});
119+
120+
let expected = expect![[
121+
r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mError validating field `a` in model `B`: The relation field `a` on model `B` is missing an opposite relation field on the model `A`. Either run `prisma format` or add it manually.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mb.prisma:4\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 3 | \u001b[0m id String @id\n\u001b[1;94m 4 | \u001b[0m \u001b[1;91ma A?\u001b[0m\n\u001b[1;94m 5 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"#
122+
]];
123+
124+
let response = merge_schemas(&request.to_string()).unwrap_err();
125+
expected.assert_eq(&response);
126+
}
127+
}

prisma-fmt/src/schema_file_input.rs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use psl::SourceFile;
2+
use serde::Deserialize;
3+
4+
/// Struct for supporting multiple files
5+
/// in a backward-compatible way: can either accept
6+
/// a single file contents or vector of (filePath, content) tuples.
7+
/// Can be converted to the input for `psl::validate_multi_file` from
8+
/// any of the variants.
9+
#[derive(Deserialize, Debug)]
10+
#[serde(untagged)]
11+
pub(crate) enum SchemaFileInput {
12+
Single(String),
13+
Multiple(Vec<(String, String)>),
14+
}
15+
16+
impl From<SchemaFileInput> for Vec<(String, SourceFile)> {
17+
fn from(value: SchemaFileInput) -> Self {
18+
match value {
19+
SchemaFileInput::Single(content) => vec![("schema.prisma".to_owned(), content.into())],
20+
SchemaFileInput::Multiple(file_list) => file_list
21+
.into_iter()
22+
.map(|(filename, content)| (filename, content.into()))
23+
.collect(),
24+
}
25+
}
26+
}

prisma-fmt/src/validate.rs

+87-6
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
use psl::ValidatedSchema;
12
use serde::Deserialize;
23
use serde_json::json;
34
use std::fmt::Write as _;
45

6+
use crate::schema_file_input::SchemaFileInput;
7+
58
// this mirrors user_facing_errors::common::SchemaParserError
69
pub(crate) static SCHEMA_PARSER_ERROR_CODE: &str = "P1012";
710

811
#[derive(Deserialize, Debug)]
912
#[serde(rename_all = "camelCase")]
1013
struct ValidateParams {
11-
prisma_schema: String,
14+
prisma_schema: SchemaFileInput,
1215
#[serde(default)]
1316
no_color: bool,
1417
}
@@ -21,21 +24,22 @@ pub(crate) fn validate(params: &str) -> Result<(), String> {
2124
}
2225
};
2326

24-
run(&params.prisma_schema, params.no_color)
27+
run(params.prisma_schema, params.no_color)?;
28+
Ok(())
2529
}
2630

27-
pub fn run(input_schema: &str, no_color: bool) -> Result<(), String> {
28-
let validate_schema = psl::validate(input_schema.into());
31+
pub fn run(input_schema: SchemaFileInput, no_color: bool) -> Result<ValidatedSchema, String> {
32+
let validate_schema = psl::validate_multi_file(input_schema.into());
2933
let diagnostics = &validate_schema.diagnostics;
3034

3135
if !diagnostics.has_errors() {
32-
return Ok(());
36+
return Ok(validate_schema);
3337
}
3438

3539
// always colorise output regardless of the environment, which is important for Wasm
3640
colored::control::set_override(!no_color);
3741

38-
let mut formatted_error = diagnostics.to_pretty_string("schema.prisma", input_schema);
42+
let mut formatted_error = validate_schema.render_own_diagnostics();
3943
write!(
4044
formatted_error,
4145
"\nValidation Error Count: {}",
@@ -109,6 +113,83 @@ mod tests {
109113
validate(&request.to_string()).unwrap();
110114
}
111115

116+
#[test]
117+
fn validate_multiple_files() {
118+
let schema = vec![
119+
(
120+
"a.prisma",
121+
r#"
122+
datasource thedb {
123+
provider = "postgresql"
124+
url = env("DBURL")
125+
}
126+
127+
model A {
128+
id String @id
129+
b_id String @unique
130+
b B @relation(fields: [b_id], references: [id])
131+
}
132+
"#,
133+
),
134+
(
135+
"b.prisma",
136+
r#"
137+
model B {
138+
id String @id
139+
a A?
140+
}
141+
"#,
142+
),
143+
];
144+
145+
let request = json!({
146+
"prismaSchema": schema,
147+
});
148+
149+
validate(&request.to_string()).unwrap();
150+
}
151+
152+
#[test]
153+
fn validate_multiple_files_error() {
154+
let schema = vec![
155+
(
156+
"a.prisma",
157+
r#"
158+
datasource thedb {
159+
provider = "postgresql"
160+
url = env("DBURL")
161+
}
162+
163+
model A {
164+
id String @id
165+
b_id String @unique
166+
b B @relation(fields: [b_id], references: [id])
167+
}
168+
"#,
169+
),
170+
(
171+
"b.prisma",
172+
r#"
173+
model B {
174+
id String @id
175+
a A
176+
}
177+
"#,
178+
),
179+
];
180+
181+
let request = json!({
182+
"prismaSchema": schema,
183+
});
184+
185+
let expected = expect![[
186+
r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mError parsing attribute \"@relation\": The relation field `a` on Model `B` is required. This is no longer valid because it's not possible to enforce this constraint on the database level. Please change the field type from `A` to `A?` to fix this.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mb.prisma:4\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 3 | \u001b[0m id String @id\n\u001b[1;94m 4 | \u001b[0m \u001b[1;91ma A\u001b[0m\n\u001b[1;94m 5 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"#
187+
]];
188+
189+
let response = validate(&request.to_string()).unwrap_err();
190+
expected.assert_eq(&response);
191+
}
192+
112193
#[test]
113194
fn validate_using_both_relation_mode_and_referential_integrity() {
114195
let schema = r#"

0 commit comments

Comments
 (0)