Skip to content

Commit

Permalink
Experiment with multi-file schema handling in PSL (#4243)
Browse files Browse the repository at this point in the history
* 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 <jkomyno@users.noreply.github.com>
Co-authored-by: jkomyno <skiabo97@gmail.com>
  • Loading branch information
3 people committed Apr 8, 2024
1 parent e66d30d commit dcdb692
Show file tree
Hide file tree
Showing 87 changed files with 967 additions and 554 deletions.
33 changes: 22 additions & 11 deletions prisma-fmt/src/code_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec

let datasource = config.datasources.first();

for source in validated_schema.db.ast().sources() {
relation_mode::edit_referential_integrity(&mut actions, &params, validated_schema.db.source(), source)
for source in validated_schema.db.ast_assert_single().sources() {
relation_mode::edit_referential_integrity(
&mut actions,
&params,
validated_schema.db.source_assert_single(),
source,
)
}

// models AND views
Expand All @@ -45,21 +50,27 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
multi_schema::add_schema_block_attribute_model(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
config,
model,
);

multi_schema::add_schema_to_schemas(&mut actions, &params, validated_schema.db.source(), config, model);
multi_schema::add_schema_to_schemas(
&mut actions,
&params,
validated_schema.db.source_assert_single(),
config,
model,
);
}

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

mongodb::add_native_for_auto_id(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
model,
datasource.unwrap(),
);
Expand All @@ -71,7 +82,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
multi_schema::add_schema_block_attribute_enum(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
config,
enumerator,
)
Expand All @@ -88,15 +99,15 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
relations::add_referenced_side_unique(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
complete_relation,
);

if relation.is_one_to_one() {
relations::add_referencing_side_unique(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
complete_relation,
);
}
Expand All @@ -105,7 +116,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
relations::add_index_for_relation_fields(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
complete_relation.referencing_field(),
);
}
Expand All @@ -114,7 +125,7 @@ pub(crate) fn available_actions(schema: String, params: CodeActionParams) -> Vec
relation_mode::replace_set_default_mysql(
&mut actions,
&params,
validated_schema.db.source(),
validated_schema.db.source_assert_single(),
complete_relation,
config,
)
Expand Down
2 changes: 1 addition & 1 deletion prisma-fmt/src/code_actions/multi_schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ pub(super) fn add_schema_to_schemas(
formatted_attribute,
true,
// 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
Span::new(span.start, span.end - 1),
Span::new(span.start, span.end - 1, psl::parser_database::FileId::ZERO),
params,
)
}
Expand Down
2 changes: 1 addition & 1 deletion prisma-fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ pub(crate) fn range_to_span(range: Range, document: &str) -> ast::Span {
let start = position_to_offset(&range.start, document).unwrap();
let end = position_to_offset(&range.end, document).unwrap();

ast::Span::new(start, end)
ast::Span::new(start, end, psl::parser_database::FileId::ZERO)
}

/// Gives the LSP position right after the given span.
Expand Down
6 changes: 3 additions & 3 deletions prisma-fmt/src/text_document_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub(crate) fn completion(schema: String, params: CompletionParams) -> Completion

let db = {
let mut diag = Diagnostics::new();
ParserDatabase::new(source_file, &mut diag)
ParserDatabase::new_single_file(source_file, &mut diag)
};

let ctx = CompletionContext {
Expand Down Expand Up @@ -91,7 +91,7 @@ impl<'a> CompletionContext<'a> {
}

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

fn push_namespaces(ctx: CompletionContext<'_>, completion_list: &mut CompletionList) {
for (namespace, _) in ctx.namespaces() {
let insert_text = if add_quotes(ctx.params, ctx.db.source()) {
let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) {
format!(r#""{namespace}""#)
} else {
namespace.to_string()
Expand Down
2 changes: 1 addition & 1 deletion prisma-fmt/src/text_document_completion/datasource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ pub(super) fn url_env_db_completion(completion_list: &mut CompletionList, kind:
_ => unreachable!(),
};

let insert_text = if add_quotes(ctx.params, ctx.db.source()) {
let insert_text = if add_quotes(ctx.params, ctx.db.source_assert_single()) {
format!(r#""{text}""#)
} else {
text.to_owned()
Expand Down
8 changes: 4 additions & 4 deletions prisma-fmt/tests/code_actions/test_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ fn parse_schema_diagnostics(file: impl Into<SourceFile>) -> Option<Vec<Diagnosti
severity: Some(DiagnosticSeverity::WARNING),
message: warn.message().to_owned(),
range: lsp_types::Range {
start: offset_to_position(warn.span().start, schema.db.source()),
end: offset_to_position(warn.span().end, schema.db.source()),
start: offset_to_position(warn.span().start, schema.db.source_assert_single()),
end: offset_to_position(warn.span().end, schema.db.source_assert_single()),
},
..Default::default()
});
Expand All @@ -31,8 +31,8 @@ fn parse_schema_diagnostics(file: impl Into<SourceFile>) -> Option<Vec<Diagnosti
severity: Some(DiagnosticSeverity::ERROR),
message: error.message().to_owned(),
range: lsp_types::Range {
start: offset_to_position(error.span().start, schema.db.source()),
end: offset_to_position(error.span().end, schema.db.source()),
start: offset_to_position(error.span().start, schema.db.source_assert_single()),
end: offset_to_position(error.span().end, schema.db.source_assert_single()),
},
..Default::default()
});
Expand Down
2 changes: 1 addition & 1 deletion psl/diagnostics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ mod warning;
pub use collection::Diagnostics;
pub use error::DatamodelError;
pub use native_type_error_factory::NativeTypeErrorFactory;
pub use span::Span;
pub use span::{FileId, Span};
pub use warning::DatamodelWarning;
26 changes: 21 additions & 5 deletions psl/diagnostics/src/span.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
/// The stable identifier for a PSL file.
#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq, PartialOrd, Ord)]
pub struct FileId(pub u32); // we can't encapsulate because it would be a circular crate
// dependency between diagnostics and parser-database

impl FileId {
pub const ZERO: FileId = FileId(0);
pub const MAX: FileId = FileId(u32::MAX);
}

/// Represents a location in a datamodel's text representation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub start: usize,
pub end: usize,
pub file_id: FileId,
}

impl Span {
/// Constructor.
pub fn new(start: usize, end: usize) -> Span {
Span { start, end }
pub fn new(start: usize, end: usize, file_id: FileId) -> Span {
Span { start, end, file_id }
}

/// Creates a new empty span.
pub fn empty() -> Span {
Span { start: 0, end: 0 }
Span {
start: 0,
end: 0,
file_id: FileId::ZERO,
}
}

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

impl From<pest::Span<'_>> for Span {
fn from(s: pest::Span<'_>) -> Self {
impl From<(FileId, pest::Span<'_>)> for Span {
fn from((file_id, s): (FileId, pest::Span<'_>)) -> Self {
Span {
start: s.start(),
end: s.end(),
file_id,
}
}
}
Loading

0 comments on commit dcdb692

Please sign in to comment.