Skip to content

Commit dcdb692

Browse files
tomhoulejkomyno
andauthored
Experiment with multi-file schema handling in PSL (#4243)
* 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). * chore(prisma-fmt): fix typo * chore(prisma-fmt): add comment * chore(prisma-fmt): fix compilation after #4137 --------- Co-authored-by: Alberto Schiabel <[email protected]> Co-authored-by: jkomyno <[email protected]>
1 parent e66d30d commit dcdb692

File tree

87 files changed

+967
-554
lines changed

Some content is hidden

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

87 files changed

+967
-554
lines changed

prisma-fmt/src/code_actions.rs

+22-11
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,13 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
3131

3232
let datasource = config.datasources.first();
3333

34-
for source in validated_schema.db.ast().sources() {
35-
relation_mode::edit_referential_integrity(&mut actions, &params, validated_schema.db.source(), source)
34+
for source in validated_schema.db.ast_assert_single().sources() {
35+
relation_mode::edit_referential_integrity(
36+
&mut actions,
37+
&params,
38+
validated_schema.db.source_assert_single(),
39+
source,
40+
)
3641
}
3742

3843
// models AND views
@@ -45,21 +50,27 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
4550
multi_schema::add_schema_block_attribute_model(
4651
&mut actions,
4752
&params,
48-
validated_schema.db.source(),
53+
validated_schema.db.source_assert_single(),
4954
config,
5055
model,
5156
);
5257

53-
multi_schema::add_schema_to_schemas(&mut actions, &params, validated_schema.db.source(), config, model);
58+
multi_schema::add_schema_to_schemas(
59+
&mut actions,
60+
&params,
61+
validated_schema.db.source_assert_single(),
62+
config,
63+
model,
64+
);
5465
}
5566

5667
if matches!(datasource, Some(ds) if ds.active_provider == "mongodb") {
57-
mongodb::add_at_map_for_id(&mut actions, &params, validated_schema.db.source(), model);
68+
mongodb::add_at_map_for_id(&mut actions, &params, validated_schema.db.source_assert_single(), model);
5869

5970
mongodb::add_native_for_auto_id(
6071
&mut actions,
6172
&params,
62-
validated_schema.db.source(),
73+
validated_schema.db.source_assert_single(),
6374
model,
6475
datasource.unwrap(),
6576
);
@@ -71,7 +82,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
7182
multi_schema::add_schema_block_attribute_enum(
7283
&mut actions,
7384
&params,
74-
validated_schema.db.source(),
85+
validated_schema.db.source_assert_single(),
7586
config,
7687
enumerator,
7788
)
@@ -88,15 +99,15 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
8899
relations::add_referenced_side_unique(
89100
&mut actions,
90101
&params,
91-
validated_schema.db.source(),
102+
validated_schema.db.source_assert_single(),
92103
complete_relation,
93104
);
94105

95106
if relation.is_one_to_one() {
96107
relations::add_referencing_side_unique(
97108
&mut actions,
98109
&params,
99-
validated_schema.db.source(),
110+
validated_schema.db.source_assert_single(),
100111
complete_relation,
101112
);
102113
}
@@ -105,7 +116,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
105116
relations::add_index_for_relation_fields(
106117
&mut actions,
107118
&params,
108-
validated_schema.db.source(),
119+
validated_schema.db.source_assert_single(),
109120
complete_relation.referencing_field(),
110121
);
111122
}
@@ -114,7 +125,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
114125
relation_mode::replace_set_default_mysql(
115126
&mut actions,
116127
&params,
117-
validated_schema.db.source(),
128+
validated_schema.db.source_assert_single(),
118129
complete_relation,
119130
config,
120131
)

prisma-fmt/src/code_actions/multi_schema.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ pub(super) fn add_schema_to_schemas(
142142
formatted_attribute,
143143
true,
144144
// todo: update spans so that we can just append to the end of the _inside_ of the array. Instead of needing to re-append the `]` or taking the span end -1
145-
Span::new(span.start, span.end - 1),
145+
Span::new(span.start, span.end - 1, psl::parser_database::FileId::ZERO),
146146
params,
147147
)
148148
}

prisma-fmt/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ pub(crate) fn range_to_span(range: Range, document: &str) -> ast::Span {
225225
let start = position_to_offset(&range.start, document).unwrap();
226226
let end = position_to_offset(&range.end, document).unwrap();
227227

228-
ast::Span::new(start, end)
228+
ast::Span::new(start, end, psl::parser_database::FileId::ZERO)
229229
}
230230

231231
/// Gives the LSP position right after the given span.

prisma-fmt/src/text_document_completion.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(crate) fn completion(schema: String, params: CompletionParams) -> Completion
4141

4242
let db = {
4343
let mut diag = Diagnostics::new();
44-
ParserDatabase::new(source_file, &mut diag)
44+
ParserDatabase::new_single_file(source_file, &mut diag)
4545
};
4646

4747
let ctx = CompletionContext {
@@ -91,7 +91,7 @@ impl<'a> CompletionContext<'a> {
9191
}
9292

9393
fn push_ast_completions(ctx: CompletionContext<'_>, completion_list: &mut CompletionList) {
94-
match ctx.db.ast().find_at_position(ctx.position) {
94+
match ctx.db.ast_assert_single().find_at_position(ctx.position) {
9595
ast::SchemaPosition::Model(
9696
_model_id,
9797
ast::ModelPosition::Field(_, ast::FieldPosition::Attribute("relation", _, Some(attr_name))),
@@ -190,7 +190,7 @@ fn ds_has_prop(ctx: CompletionContext<'_>, prop: &str) -> bool {
190190

191191
fn push_namespaces(ctx: CompletionContext<'_>, completion_list: &mut CompletionList) {
192192
for (namespace, _) in ctx.namespaces() {
193-
let insert_text = if add_quotes(ctx.params, ctx.db.source()) {
193+
let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) {
194194
format!(r#""{namespace}""#)
195195
} else {
196196
namespace.to_string()

prisma-fmt/src/text_document_completion/datasource.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub(super) fn url_env_db_completion(completion_list: &mut CompletionList, kind:
144144
_ => unreachable!(),
145145
};
146146

147-
let insert_text = if add_quotes(ctx.params, ctx.db.source()) {
147+
let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) {
148148
format!(r#""{text}""#)
149149
} else {
150150
text.to_owned()

prisma-fmt/tests/code_actions/test_api.rs

+4-4
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ fn parse_schema_diagnostics(file: impl Into<SourceFile>) -> Option<Vec<Diagnosti
1919
severity: Some(DiagnosticSeverity::WARNING),
2020
message: warn.message().to_owned(),
2121
range: lsp_types::Range {
22-
start: offset_to_position(warn.span().start, schema.db.source()),
23-
end: offset_to_position(warn.span().end, schema.db.source()),
22+
start: offset_to_position(warn.span().start, schema.db.source_assert_single()),
23+
end: offset_to_position(warn.span().end, schema.db.source_assert_single()),
2424
},
2525
..Default::default()
2626
});
@@ -31,8 +31,8 @@ fn parse_schema_diagnostics(file: impl Into<SourceFile>) -> Option<Vec<Diagnosti
3131
severity: Some(DiagnosticSeverity::ERROR),
3232
message: error.message().to_owned(),
3333
range: lsp_types::Range {
34-
start: offset_to_position(error.span().start, schema.db.source()),
35-
end: offset_to_position(error.span().end, schema.db.source()),
34+
start: offset_to_position(error.span().start, schema.db.source_assert_single()),
35+
end: offset_to_position(error.span().end, schema.db.source_assert_single()),
3636
},
3737
..Default::default()
3838
});

psl/diagnostics/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ mod warning;
88
pub use collection::Diagnostics;
99
pub use error::DatamodelError;
1010
pub use native_type_error_factory::NativeTypeErrorFactory;
11-
pub use span::Span;
11+
pub use span::{FileId, Span};
1212
pub use warning::DatamodelWarning;

psl/diagnostics/src/span.rs

+21-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
1+
/// The stable identifier for a PSL file.
2+
#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq, PartialOrd, Ord)]
3+
pub struct FileId(pub u32); // we can't encapsulate because it would be a circular crate
4+
// dependency between diagnostics and parser-database
5+
6+
impl FileId {
7+
pub const ZERO: FileId = FileId(0);
8+
pub const MAX: FileId = FileId(u32::MAX);
9+
}
10+
111
/// Represents a location in a datamodel's text representation.
212
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313
pub struct Span {
414
pub start: usize,
515
pub end: usize,
16+
pub file_id: FileId,
617
}
718

819
impl Span {
920
/// Constructor.
10-
pub fn new(start: usize, end: usize) -> Span {
11-
Span { start, end }
21+
pub fn new(start: usize, end: usize, file_id: FileId) -> Span {
22+
Span { start, end, file_id }
1223
}
1324

1425
/// Creates a new empty span.
1526
pub fn empty() -> Span {
16-
Span { start: 0, end: 0 }
27+
Span {
28+
start: 0,
29+
end: 0,
30+
file_id: FileId::ZERO,
31+
}
1732
}
1833

1934
/// Is the given position inside the span? (boundaries included)
@@ -27,11 +42,12 @@ impl Span {
2742
}
2843
}
2944

30-
impl From<pest::Span<'_>> for Span {
31-
fn from(s: pest::Span<'_>) -> Self {
45+
impl From<(FileId, pest::Span<'_>)> for Span {
46+
fn from((file_id, s): (FileId, pest::Span<'_>)) -> Self {
3247
Span {
3348
start: s.start(),
3449
end: s.end(),
50+
file_id,
3551
}
3652
}
3753
}

0 commit comments

Comments
 (0)