diff --git a/prisma-fmt/src/hover.rs b/prisma-fmt/src/hover.rs new file mode 100644 index 000000000000..ae2eaa28e1da --- /dev/null +++ b/prisma-fmt/src/hover.rs @@ -0,0 +1,244 @@ +use log::warn; +use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; +use psl::{ + error_tolerant_parse_configuration, + parser_database::{ + walkers::{self, Walker}, + ParserDatabase, RelationFieldId, ScalarFieldType, + }, + schema_ast::ast::{ + self, CompositeTypePosition, EnumPosition, EnumValuePosition, Field, FieldPosition, ModelPosition, + SchemaPosition, WithDocumentation, WithName, + }, + Diagnostics, SourceFile, +}; + +use crate::{offsets::position_to_offset, LSPContext}; + +pub(super) type HoverContext<'a> = LSPContext<'a, HoverParams>; + +impl<'a> HoverContext<'a> { + pub(super) fn position(&self) -> Option { + let pos = self.params.text_document_position_params.position; + let initiating_doc = self.initiating_file_source(); + + position_to_offset(&pos, initiating_doc) + } +} + +pub fn run(schema_files: Vec<(String, SourceFile)>, params: HoverParams) -> Option { + let (_, config, _) = error_tolerant_parse_configuration(&schema_files); + + let db = { + let mut diag = Diagnostics::new(); + ParserDatabase::new(&schema_files, &mut diag) + }; + + let Some(initiating_file_id) = db.file_id(params.text_document_position_params.text_document.uri.as_str()) else { + warn!("Initiating file name is not found in the schema"); + return None; + }; + + let ctx = HoverContext { + db: &db, + config: &config, + initiating_file_id, + params: ¶ms, + }; + + hover(ctx) +} + +fn hover(ctx: HoverContext<'_>) -> Option { + let position = match ctx.position() { + Some(pos) => pos, + None => { + warn!("Received a position outside of the document boundaries in HoverParams"); + return None; + } + }; + + let ast = ctx.db.ast(ctx.initiating_file_id); + let contents = match ast.find_at_position(position) { + SchemaPosition::TopLevel => None, + + // --- Block Names --- + SchemaPosition::Model(model_id, ModelPosition::Name(name)) => { + let model = ctx.db.walk((ctx.initiating_file_id, model_id)).ast_model(); + let variant = if model.is_view() { "view" } else { "model" }; + + Some(format_hover_content( + model.documentation().unwrap_or(""), + variant, + name, + None, + )) + } + SchemaPosition::Enum(enum_id, EnumPosition::Name(name)) => { + let enm = ctx.db.walk((ctx.initiating_file_id, enum_id)).ast_enum(); + Some(hover_enum(enm, name)) + } + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Name(name)) => { + let ct = ctx.db.walk((ctx.initiating_file_id, ct_id)).ast_composite_type(); + Some(hover_composite(ct, name)) + } + + // --- Block Field Names --- + SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Name(name))) => { + let field = ctx + .db + .walk((ctx.initiating_file_id, model_id)) + .field(field_id) + .ast_field(); + + Some(format_hover_content( + field.documentation().unwrap_or_default(), + "field", + name, + None, + )) + } + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Name(name))) => { + let field = ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id).ast_field(); + + Some(format_hover_content( + field.documentation().unwrap_or_default(), + "field", + name, + None, + )) + } + SchemaPosition::Enum(enm_id, EnumPosition::Value(value_id, EnumValuePosition::Name(name))) => { + let value = ctx + .db + .walk((ctx.initiating_file_id, enm_id)) + .value(value_id) + .ast_value(); + + Some(format_hover_content( + value.documentation().unwrap_or_default(), + "value", + name, + None, + )) + } + + // --- Block Field Types --- + SchemaPosition::Model(model_id, ModelPosition::Field(field_id, FieldPosition::Type(name))) => { + let initiating_field = &ctx.db.walk((ctx.initiating_file_id, model_id)).field(field_id); + + initiating_field.refine().and_then(|field| match field { + walkers::RefinedFieldWalker::Scalar(scalar) => match scalar.scalar_field_type() { + ScalarFieldType::CompositeType(_) => { + let ct = scalar.field_type_as_composite_type().unwrap().ast_composite_type(); + Some(hover_composite(ct, ct.name())) + } + ScalarFieldType::Enum(_) => { + let enm = scalar.field_type_as_enum().unwrap().ast_enum(); + Some(hover_enum(enm, enm.name())) + } + _ => None, + }, + walkers::RefinedFieldWalker::Relation(rf) => { + let opposite_model = rf.related_model(); + let relation_info = rf.opposite_relation_field().map(|rf| (rf, rf.ast_field())); + let related_model_type = if opposite_model.ast_model().is_view() { + "view" + } else { + "model" + }; + + Some(format_hover_content( + opposite_model.ast_model().documentation().unwrap_or_default(), + related_model_type, + name, + relation_info, + )) + } + }) + } + + SchemaPosition::CompositeType(ct_id, CompositeTypePosition::Field(field_id, FieldPosition::Type(_))) => { + let field = &ctx.db.walk((ctx.initiating_file_id, ct_id)).field(field_id); + match field.r#type() { + psl::parser_database::ScalarFieldType::CompositeType(_) => { + let ct = field.field_type_as_composite_type().unwrap().ast_composite_type(); + Some(hover_composite(ct, ct.name())) + } + psl::parser_database::ScalarFieldType::Enum(_) => { + let enm = field.field_type_as_enum().unwrap().ast_enum(); + Some(hover_enum(enm, enm.name())) + } + _ => None, + } + } + _ => None, + }; + + contents.map(|contents| Hover { contents, range: None }) +} + +fn hover_enum(enm: &ast::Enum, name: &str) -> HoverContents { + format_hover_content(enm.documentation().unwrap_or_default(), "enum", name, None) +} + +fn hover_composite(ct: &ast::CompositeType, name: &str) -> HoverContents { + format_hover_content(ct.documentation().unwrap_or_default(), "type", name, None) +} + +fn format_hover_content( + documentation: &str, + variant: &str, + name: &str, + relation: Option<(Walker, &Field)>, +) -> HoverContents { + let fancy_line_break = String::from("\n___\n"); + + let (field, relation_kind) = format_relation_info(relation, &fancy_line_break); + + let prisma_display = match variant { + "model" | "enum" | "view" | "type" => { + format!("```prisma\n{variant} {name} {{{field}}}\n```{fancy_line_break}{relation_kind}") + } + "field" | "value" => format!("```prisma\n{name}\n```{fancy_line_break}"), + _ => "".to_owned(), + }; + let full_signature = format!("{prisma_display}{documentation}"); + + HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: full_signature, + }) +} + +fn format_relation_info( + relation: Option<(Walker, &Field)>, + fancy_line_break: &String, +) -> (String, String) { + if let Some((rf, field)) = relation { + let relation = rf.relation(); + + let fields = rf + .referencing_fields() + .map(|fields| fields.map(|f| f.to_string()).collect::>().join(", ")) + .map_or_else(String::new, |fields| format!(", fields: [{fields}]")); + + let references = rf + .referenced_fields() + .map(|fields| fields.map(|f| f.to_string()).collect::>().join(", ")) + .map_or_else(String::new, |fields| format!(", references: [{fields}]")); + + let self_relation = if relation.is_self_relation() { " on self" } else { "" }; + let relation_kind = format!("{}{}", relation.relation_kind(), self_relation); + + let relation_name = relation.relation_name(); + let relation_inner = format!("name: \"{relation_name}\"{fields}{references}"); + + ( + format!("\n\t...\n\t{field} @relation({relation_inner})\n"), + format!("{relation_kind}{fancy_line_break}"), + ) + } else { + ("".to_owned(), "".to_owned()) + } +} diff --git a/prisma-fmt/src/lib.rs b/prisma-fmt/src/lib.rs index e21c6eef97e9..b6b13c47838f 100644 --- a/prisma-fmt/src/lib.rs +++ b/prisma-fmt/src/lib.rs @@ -3,23 +3,25 @@ mod code_actions; mod get_config; mod get_datamodel; mod get_dmmf; +mod hover; mod lint; mod merge_schemas; mod native; -mod offsets; mod preview; mod references; mod schema_file_input; mod text_document_completion; mod validate; +pub mod offsets; + use log::*; -pub use offsets::span_to_range; use psl::{ datamodel_connector::Connector, diagnostics::FileId, parser_database::ParserDatabase, Configuration, Datasource, Generator, }; use schema_file_input::SchemaFileInput; +use serde_json::json; #[derive(Debug, Clone, Copy)] pub(crate) struct LSPContext<'a, T> { @@ -110,6 +112,28 @@ pub fn references(schema_files: String, params: &str) -> String { serde_json::to_string(&references).unwrap() } +pub fn hover(schema_files: String, params: &str) -> String { + let schema: SchemaFileInput = match serde_json::from_str(&schema_files) { + Ok(schema) => schema, + Err(serde_err) => { + warn!("Failed to deserialize SchemaFileInput: {serde_err}"); + return json!(null).to_string(); + } + }; + + let params: lsp_types::HoverParams = match serde_json::from_str(params) { + Ok(params) => params, + Err(_) => { + warn!("Failed to deserialize Hover"); + return json!(null).to_string(); + } + }; + + let hover = hover::run(schema.into(), params); + + serde_json::to_string(&hover).unwrap() +} + /// The two parameters are: /// - The [`SchemaFileInput`] to reformat, as a string. /// - An LSP diff --git a/prisma-fmt/src/references.rs b/prisma-fmt/src/references.rs index 00da04548893..0fb3b3a665ad 100644 --- a/prisma-fmt/src/references.rs +++ b/prisma-fmt/src/references.rs @@ -11,7 +11,10 @@ use psl::{ Diagnostics, SourceFile, }; -use crate::{offsets::position_to_offset, span_to_range, LSPContext}; +use crate::{ + offsets::{position_to_offset, span_to_range}, + LSPContext, +}; pub(super) type ReferencesContext<'a> = LSPContext<'a, ReferenceParams>; diff --git a/prisma-fmt/tests/code_actions/test_api.rs b/prisma-fmt/tests/code_actions/test_api.rs index 396dfd103a2c..b09f517be9c5 100644 --- a/prisma-fmt/tests/code_actions/test_api.rs +++ b/prisma-fmt/tests/code_actions/test_api.rs @@ -1,6 +1,7 @@ use lsp_types::{Diagnostic, DiagnosticSeverity}; use once_cell::sync::Lazy; -use prisma_fmt::span_to_range; + +use prisma_fmt::offsets::span_to_range; use psl::{diagnostics::Span, SourceFile}; use std::{fmt::Write as _, io::Write as _, path::PathBuf}; diff --git a/prisma-fmt/tests/hover/mod.rs b/prisma-fmt/tests/hover/mod.rs new file mode 100644 index 000000000000..cf3a59fec326 --- /dev/null +++ b/prisma-fmt/tests/hover/mod.rs @@ -0,0 +1,2 @@ +mod test_api; +mod tests; diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json new file mode 100644 index 000000000000..db452d702e44 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\ntype TypeA {}\n```\n___\nThis is doc for TypeA" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma new file mode 100644 index 000000000000..2f0102f18cf2 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_block_name/schema.prisma @@ -0,0 +1,19 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model ModelNameA { + id String @id @map("_id") + bId Int + val TypeA +} + +/// This is doc for TypeA +type Typ<|>eA { +id String +} diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json new file mode 100644 index 000000000000..fe56452d5653 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\ntype Address {}\n```\n___\nAddress Doc" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma new file mode 100644 index 000000000000..e43eca95795e --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/composite_from_field_type/schema.prisma @@ -0,0 +1,14 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @map("_id") + address Add<|>ress +} + +/// Address Doc +type Address { + street String +} diff --git a/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json new file mode 100644 index 000000000000..6c3e8a803c01 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Animals {\n\t...\n\tfamily Humans[] @relation(name: \"AnimalsToHumans\", fields: [humanIds], references: [id])\n}\n```\n___\nimplicit many-to-many\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma new file mode 100644 index 000000000000..0a3827e1490c --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/embedded_m2n_mongodb/schema.prisma @@ -0,0 +1,16 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model Humans { + id String @id @default(auto()) @map("_id") @db.ObjectId + animalIds String[] @db.ObjectId + family An<|>imals[] @relation(fields: [animalIds], references: [id]) +} + +model Animals { + id String @id @default(auto()) @map("_id") @db.ObjectId + humanIds String[] @db.ObjectId + family Humans[] @relation(fields: [humanIds], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json new file mode 100644 index 000000000000..7d48788f415c --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nenum Poly {}\n```\n___\nThis is doc for B" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma new file mode 100644 index 000000000000..a0e46e50934f --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_block_name/schema.prisma @@ -0,0 +1,20 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model ModelNameA { + id Int @id + + poly Poly +} + +/// This is doc for B +enum Po<|>ly { +Am +Nesia +} diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json new file mode 100644 index 000000000000..a072fe175d82 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nenum Animal {}\n```\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma new file mode 100644 index 000000000000..8f1b6cfdc826 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/enum_from_field_type/schema.prisma @@ -0,0 +1,16 @@ +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model User { + id String @id @map("_id") + pet Ani<|>mal +} + +// Animal Doc +enum Animal { + REDPANDA + CAT + DOG +} diff --git a/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json new file mode 100644 index 000000000000..5fa4df924f54 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nveryImportantField\n```\n___\nvery important documentation" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma new file mode 100644 index 000000000000..ead9dd9ef278 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_composite_field_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +type ModelNameA { + likesRedPandas Boolean + + /// very important documentation + veryImpor<|>tantField DateTime +} diff --git a/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json new file mode 100644 index 000000000000..5fa4df924f54 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nveryImportantField\n```\n___\nvery important documentation" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma new file mode 100644 index 000000000000..f05c1b1e48d5 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/field_from_model_field_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +model ModelNameA { + id String @id @map("_id") + + /// very important documentation + veryImpor<|>tantField DateTime +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json b/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json new file mode 100644 index 000000000000..7f77aa514659 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_block_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel ModelNameB {}\n```\n___\nThis is doc for B" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma new file mode 100644 index 000000000000..632ac7e0ce48 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_block_name/schema.prisma @@ -0,0 +1,20 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model ModelNameA { + id Int @id + bId Int + val ModelNameB @relation(fields: [bId], references: [id]) +} + +/// This is doc for B +model Model<|>NameB { +id Int @id +A ModelNameA[] +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json new file mode 100644 index 000000000000..f99f30abaf57 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Post {\n\t...\n\tUser User? @relation(name: \"PostToUser\", fields: [userId], references: [id])\n}\n```\n___\none-to-many\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma new file mode 100644 index 000000000000..2b9d835c2b50 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_includes_broken_relations/schema.prisma @@ -0,0 +1,26 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id + + posts Po<|>st[] +} + +model Post { + id Int @id + + content String + + userId Int? + User User? @relation(fields: [userId], references: [id]) +} + +model interm { + id Int @id + + forumId Int + forum Forum @relation(fields: [forumId], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json new file mode 100644 index 000000000000..ec747fa47ddb --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/result.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma new file mode 100644 index 000000000000..bfd01e22c868 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_model_type_on_broken_relations/schema.prisma @@ -0,0 +1,12 @@ +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model interm { + id Int @id + + forumId Int + forum For<|>um @relation(fields: [forumId], references: [id]) +} + diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma new file mode 100644 index 000000000000..28c6cb9d2695 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/a.prisma @@ -0,0 +1,6 @@ +/// This is doc for A +model ModelNameA { + id Int @id + bId Int + val ModelNameB @relation(fields: [bId], references: [id]) +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma new file mode 100644 index 000000000000..934a52fcf33d --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/b.prisma @@ -0,0 +1,5 @@ +/// This is doc for B +view ModelNameB { + id Int @id + A ModelNa<|>meA[] +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma b/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma new file mode 100644 index 000000000000..654be6eef819 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/config.prisma @@ -0,0 +1,9 @@ +generator js { + provider = "prisma-client-js" + previewFeatures = ["views", "prismaSchemaFolder"] +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} diff --git a/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json b/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json new file mode 100644 index 000000000000..fd05a78439e1 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/model_from_view_type/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel ModelNameA {\n\t...\n\tval ModelNameB @relation(name: \"ModelNameAToModelNameB\", fields: [bId], references: [id])\n}\n```\n___\none-to-many\n___\nThis is doc for A" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json new file mode 100644 index 000000000000..5663663e4274 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nmodel Bee {\n\t...\n\tB Bee? @relation(name: \"bees\", fields: [bId], references: [id])\n}\n```\n___\none-to-many on self\n___\n" + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma new file mode 100644 index 000000000000..f6b52dbe8d81 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/one_to_many_self_relation/schema.prisma @@ -0,0 +1,6 @@ +model Bee { + id Int @id + bees Be<|>e[] @relation(name: "bees") + B Bee? @relation(name: "bees", fields: [bId], references: [id]) + bId Int? +} diff --git a/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json new file mode 100644 index 000000000000..2f8ef36c7f24 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/result.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "```prisma\nRedPanda\n```\n___\nRedpandas are super cute." + } +} \ No newline at end of file diff --git a/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma new file mode 100644 index 000000000000..8b339228b780 --- /dev/null +++ b/prisma-fmt/tests/hover/scenarios/value_from_enum_value_name/schema.prisma @@ -0,0 +1,15 @@ +generator js { + provider = "prisma-client-js" +} + +datasource db { + provider = "mongodb" + url = env("DATABASE_URL") +} + +/// enum doc +enum Pet { + /// Redpandas are super cute. + RedP<|>anda + Cat +} diff --git a/prisma-fmt/tests/hover/test_api.rs b/prisma-fmt/tests/hover/test_api.rs new file mode 100644 index 000000000000..598b08f8ead1 --- /dev/null +++ b/prisma-fmt/tests/hover/test_api.rs @@ -0,0 +1,144 @@ +use crate::helpers::load_schema_files; +use once_cell::sync::Lazy; +use std::{fmt::Write as _, io::Write as _}; + +const CURSOR_MARKER: &str = "<|>"; +const SCENARIOS_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/hover/scenarios"); +static UPDATE_EXPECT: Lazy = Lazy::new(|| std::env::var("UPDATE_EXPECT").is_ok()); + +pub(crate) fn test_scenario(scenario_name: &str) { + let mut path = String::with_capacity(SCENARIOS_PATH.len() + 12); + + let schema_files = { + write!(path, "{SCENARIOS_PATH}/{scenario_name}").unwrap(); + load_schema_files(&path) + }; + + path.clear(); + write!(path, "{SCENARIOS_PATH}/{scenario_name}/result.json").unwrap(); + let expected_result = std::fs::read_to_string(&path).unwrap_or_else(|_| String::new()); + + let (initiating_file_uri, cursor_position, schema_files) = take_cursor(schema_files); + + let params = lsp_types::HoverParams { + text_document_position_params: lsp_types::TextDocumentPositionParams { + text_document: lsp_types::TextDocumentIdentifier { + uri: initiating_file_uri.parse().unwrap(), + }, + position: cursor_position, + }, + work_done_progress_params: lsp_types::WorkDoneProgressParams { work_done_token: None }, + }; + + let result = prisma_fmt::hover( + serde_json::to_string_pretty(&schema_files).unwrap(), + &serde_json::to_string_pretty(¶ms).unwrap(), + ); + + // Prettify the JSON + let result = + serde_json::to_string_pretty(&serde_json::from_str::>(&result).unwrap()).unwrap(); + + if *UPDATE_EXPECT { + let mut file = std::fs::File::create(&path).unwrap(); // truncate + file.write_all(result.as_bytes()).unwrap(); + } else if expected_result != result { + let chunks = dissimilar::diff(&expected_result, &result); + panic!( + r#" +Snapshot comparison failed. Run the test again with UPDATE_EXPECT=1 in the environment to update the snapshot. + +===== EXPECTED ==== +{} +====== FOUND ====== +{} +======= DIFF ====== +{} +"#, + expected_result, + result, + format_chunks(chunks), + ); + } +} + +fn format_chunks(chunks: Vec) -> String { + let mut buf = String::new(); + for chunk in chunks { + let formatted = match chunk { + dissimilar::Chunk::Equal(text) => text.into(), + dissimilar::Chunk::Delete(text) => format!("\x1b[41m{text}\x1b[0m"), + dissimilar::Chunk::Insert(text) => format!("\x1b[42m{text}\x1b[0m"), + }; + buf.push_str(&formatted); + } + buf +} + +fn take_cursor(schema_files: Vec<(String, String)>) -> (String, lsp_types::Position, Vec<(String, String)>) { + let mut result = Vec::with_capacity(schema_files.len()); + let mut file_and_pos = None; + for (file_name, content) in schema_files { + if let Some((pos, without_cursor)) = take_cursor_one(&content) { + file_and_pos = Some((file_name.clone(), pos)); + result.push((file_name, without_cursor)); + } else { + result.push((file_name, content)); + } + } + + let (file_name, position) = file_and_pos.expect("Could not find a cursor in any of the schema files"); + + (file_name, position, result) +} + +fn take_cursor_one(schema: &str) -> Option<(lsp_types::Position, String)> { + let mut schema_without_cursor = String::with_capacity(schema.len() - 3); + let mut cursor_position = lsp_types::Position { character: 0, line: 0 }; + let mut cursor_found = false; + for line in schema.lines() { + if !cursor_found { + if let Some(pos) = line.find(CURSOR_MARKER) { + cursor_position.character = pos as u32; + cursor_found = true; + schema_without_cursor.push_str(&line[..pos]); + schema_without_cursor.push_str(&line[pos + 3..]); + schema_without_cursor.push('\n'); + } else { + schema_without_cursor.push_str(line); + schema_without_cursor.push('\n'); + cursor_position.line += 1; + } + } else { + schema_without_cursor.push_str(line); + schema_without_cursor.push('\n'); + } + } + + if !cursor_found { + return None; + } + // remove extra newline + schema_without_cursor.truncate(schema_without_cursor.len() - 1); + + Some((cursor_position, schema_without_cursor)) +} + +#[test] +fn take_cursor_works() { + let schema = r#" + model Test { + id Int @id @map(<|>) + } + "#; + let expected_schema = r#" + model Test { + id Int @id @map() + } + "#; + + let (pos, schema) = take_cursor_one(schema).unwrap(); + assert_eq!(pos.line, 2); + assert_eq!(pos.character, 28); + assert_eq!(schema, expected_schema); +} diff --git a/prisma-fmt/tests/hover/tests.rs b/prisma-fmt/tests/hover/tests.rs new file mode 100644 index 000000000000..2001dfec5b54 --- /dev/null +++ b/prisma-fmt/tests/hover/tests.rs @@ -0,0 +1,29 @@ +use super::test_api::test_scenario; + +macro_rules! scenarios { + ($($scenario_name:ident)+) => { + $( + #[test] + fn $scenario_name() { + test_scenario(stringify!($scenario_name)) + } + )* + } +} + +scenarios! { + composite_from_block_name + composite_from_field_type + embedded_m2n_mongodb + enum_from_block_name + enum_from_field_type + field_from_composite_field_name + field_from_model_field_name + model_from_block_name + model_from_model_type_includes_broken_relations + model_from_model_type_on_broken_relations + model_from_view_type + one_to_many_self_relation + value_from_enum_value_name + +} diff --git a/prisma-fmt/tests/hover_tests.rs b/prisma-fmt/tests/hover_tests.rs new file mode 100644 index 000000000000..cf416f3a1b37 --- /dev/null +++ b/prisma-fmt/tests/hover_tests.rs @@ -0,0 +1,2 @@ +mod helpers; +mod hover; diff --git a/prisma-schema-wasm/src/lib.rs b/prisma-schema-wasm/src/lib.rs index 36a2c9d353ff..8ef24cbcdfb3 100644 --- a/prisma-schema-wasm/src/lib.rs +++ b/prisma-schema-wasm/src/lib.rs @@ -123,6 +123,15 @@ pub fn references(schema: String, params: String) -> String { prisma_fmt::references(schema, ¶ms) } +/// This api is modelled on an LSP [hover request](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#hover-request-leftwards_arrow_with_hook). +/// Input and output are both JSON, the request being a `HoverParams` object +/// and the response being a `Hover` object. +#[wasm_bindgen] +pub fn hover(schema_files: String, params: String) -> String { + register_panic_hook(); + prisma_fmt::hover(schema_files, ¶ms) +} + /// Trigger a panic inside the wasm module. This is only useful in development for testing panic /// handling. #[wasm_bindgen] diff --git a/psl/parser-database/src/attributes.rs b/psl/parser-database/src/attributes.rs index 0d0bbfe786d3..b17c3d1a6676 100644 --- a/psl/parser-database/src/attributes.rs +++ b/psl/parser-database/src/attributes.rs @@ -45,7 +45,12 @@ fn resolve_composite_type_attributes<'db>( ctx: &mut Context<'db>, ) { for (field_id, field) in ct.iter_fields() { - let CompositeTypeField { r#type, .. } = ctx.types.composite_type_fields[&(ctid, field_id)]; + let CompositeTypeField { r#type, .. } = + if let Some(val) = ctx.types.composite_type_fields.get(&(ctid, field_id)) { + val.clone() + } else { + continue; + }; ctx.visit_attributes((ctid.0, (ctid.1, field_id))); @@ -81,14 +86,13 @@ fn resolve_composite_type_attributes<'db>( fn resolve_enum_attributes<'db>(enum_id: crate::EnumId, ast_enum: &'db ast::Enum, ctx: &mut Context<'db>) { let mut enum_attributes = EnumAttributes::default(); - for value_idx in 0..ast_enum.values.len() { - ctx.visit_attributes((enum_id.0, (enum_id.1, value_idx as u32))); + for (value_id, _) in ast_enum.iter_values() { + ctx.visit_attributes((enum_id.0, (enum_id.1, value_id))); // @map if ctx.visit_optional_single_attr("map") { if let Some(mapped_name) = map::visit_map_attribute(ctx) { - enum_attributes.mapped_values.insert(value_idx as u32, mapped_name); - ctx.mapped_enum_value_names - .insert((enum_id, mapped_name), value_idx as u32); + enum_attributes.mapped_values.insert(value_id, mapped_name); + ctx.mapped_enum_value_names.insert((enum_id, mapped_name), value_id); } ctx.validate_visited_arguments(); } @@ -629,15 +633,16 @@ fn common_index_validations( let mut suggested_fields = Vec::new(); for (_, field_id) in &relation_fields { - let fields = ctx + let Some(rf) = ctx .types .range_model_relation_fields(model_id) .find(|(_, rf)| rf.field_id == *field_id) - .unwrap() - .1 - .fields - .iter() - .flatten(); + else { + continue; + }; + + let fields = rf.1.fields.iter().flatten(); + for underlying_field in fields { let ScalarField { model_id, field_id, .. } = ctx.types[*underlying_field]; suggested_fields.push(ctx.asts[model_id][field_id].name()); @@ -1097,25 +1102,3 @@ fn validate_clustering_setting(ctx: &mut Context<'_>) -> Option { ctx.visit_optional_arg("clustered") .and_then(|sort| coerce::boolean(sort, ctx.diagnostics)) } - -/// Create the default values of [`ModelAttributes`] and [`EnumAttributes`] for each model and enum -/// in the AST to ensure [`crate::walkers::ModelWalker`] and [`crate::walkers::EnumWalker`] can -/// access their corresponding entries in the attributes map in the database even in the presence -/// of name and type resolution errors. This is useful for the language tools. -pub(super) fn create_default_attributes(ctx: &mut Context<'_>) { - for ((file_id, top), _) in ctx.iter_tops() { - match top { - ast::TopId::Model(model_id) => { - ctx.types - .model_attributes - .insert((file_id, model_id), ModelAttributes::default()); - } - ast::TopId::Enum(enum_id) => { - ctx.types - .enum_attributes - .insert((file_id, enum_id), EnumAttributes::default()); - } - _ => (), - } - } -} diff --git a/psl/parser-database/src/context.rs b/psl/parser-database/src/context.rs index 6d4d72239824..fb62d8a8b26a 100644 --- a/psl/parser-database/src/context.rs +++ b/psl/parser-database/src/context.rs @@ -5,7 +5,7 @@ use crate::{ ast, interner::StringInterner, names::Names, relations::Relations, types::Types, DatamodelError, Diagnostics, InFile, StringId, }; -use schema_ast::ast::{Expression, WithName}; +use schema_ast::ast::{EnumValueId, Expression, WithName}; use std::collections::{HashMap, HashSet}; /// Validation context. This is an implementation detail of ParserDatabase. It @@ -33,7 +33,7 @@ pub(crate) struct Context<'db> { pub(super) mapped_model_scalar_field_names: HashMap<(crate::ModelId, StringId), ast::FieldId>, pub(super) mapped_composite_type_names: HashMap<(crate::CompositeTypeId, StringId), ast::FieldId>, pub(super) mapped_enum_names: HashMap, - pub(super) mapped_enum_value_names: HashMap<(crate::EnumId, StringId), u32>, + pub(super) mapped_enum_value_names: HashMap<(crate::EnumId, StringId), EnumValueId>, } impl<'db> Context<'db> { diff --git a/psl/parser-database/src/lib.rs b/psl/parser-database/src/lib.rs index 5764248eff36..5ada8cebb961 100644 --- a/psl/parser-database/src/lib.rs +++ b/psl/parser-database/src/lib.rs @@ -107,35 +107,9 @@ impl ParserDatabase { // First pass: resolve names. names::resolve_names(&mut ctx); - // Return early on name resolution errors. - if ctx.diagnostics.has_errors() { - attributes::create_default_attributes(&mut ctx); - - return ParserDatabase { - asts, - interner, - names, - types, - relations, - }; - } - // Second pass: resolve top-level items and field types. types::resolve_types(&mut ctx); - // Return early on type resolution errors. - if ctx.diagnostics.has_errors() { - attributes::create_default_attributes(&mut ctx); - - return ParserDatabase { - asts, - interner, - names, - types, - relations, - }; - } - // Third pass: validate model and field attributes. All these // validations should be _order independent_ and only rely on // information from previous steps, not from other attributes. diff --git a/psl/parser-database/src/relations.rs b/psl/parser-database/src/relations.rs index 0c1e0a454c69..cbc2174f8103 100644 --- a/psl/parser-database/src/relations.rs +++ b/psl/parser-database/src/relations.rs @@ -195,6 +195,10 @@ impl Relation { matches!(self.attributes, RelationAttributes::ImplicitManyToMany { .. }) } + pub(crate) fn is_two_way_embedded_many_to_many(&self) -> bool { + matches!(self.attributes, RelationAttributes::TwoWayEmbeddedManyToMany { .. }) + } + pub(crate) fn as_complete_fields(&self) -> Option<(RelationFieldId, RelationFieldId)> { match &self.attributes { RelationAttributes::ImplicitManyToMany { field_a, field_b } => Some((*field_a, *field_b)), @@ -206,10 +210,6 @@ impl Relation { _ => None, } } - - pub(crate) fn is_two_way_embedded_many_to_many(&self) -> bool { - matches!(self.attributes, RelationAttributes::TwoWayEmbeddedManyToMany { .. }) - } } // Implementation detail for this module. Should stay private. diff --git a/psl/parser-database/src/types.rs b/psl/parser-database/src/types.rs index c7626e08649d..7d8a0c6a949f 100644 --- a/psl/parser-database/src/types.rs +++ b/psl/parser-database/src/types.rs @@ -4,7 +4,7 @@ use crate::{context::Context, interner::StringId, walkers::IndexFieldWalker, Dat use either::Either; use enumflags2::bitflags; use rustc_hash::FxHashMap as HashMap; -use schema_ast::ast::{self, WithName}; +use schema_ast::ast::{self, EnumValueId, WithName}; use std::{collections::BTreeMap, fmt}; pub(super) fn resolve_types(ctx: &mut Context<'_>) { @@ -21,6 +21,12 @@ pub(super) fn resolve_types(ctx: &mut Context<'_>) { } } +pub enum RefinedFieldVariant { + Relation(RelationFieldId), + Scalar(ScalarFieldId), + Unknown, +} + #[derive(Debug, Default)] pub(super) struct Types { pub(super) composite_type_fields: BTreeMap<(crate::CompositeTypeId, ast::FieldId), CompositeTypeField>, @@ -92,16 +98,16 @@ impl Types { .map(move |(idx, rf)| (RelationFieldId((first_relation_field_idx + idx) as u32), rf)) } - pub(super) fn refine_field(&self, id: (crate::ModelId, ast::FieldId)) -> Either { + pub(super) fn refine_field(&self, id: (crate::ModelId, ast::FieldId)) -> RefinedFieldVariant { self.relation_fields .binary_search_by_key(&id, |rf| (rf.model_id, rf.field_id)) - .map(|idx| Either::Left(RelationFieldId(idx as u32))) + .map(|idx| RefinedFieldVariant::Relation(RelationFieldId(idx as u32))) .or_else(|_| { self.scalar_fields .binary_search_by_key(&id, |sf| (sf.model_id, sf.field_id)) - .map(|id| Either::Right(ScalarFieldId(id as u32))) + .map(|id| RefinedFieldVariant::Scalar(ScalarFieldId(id as u32))) }) - .expect("expected field to be either scalar or relation field") + .unwrap_or(RefinedFieldVariant::Unknown) } pub(super) fn push_relation_field(&mut self, relation_field: RelationField) -> RelationFieldId { @@ -623,7 +629,7 @@ pub struct FieldWithArgs { pub(super) struct EnumAttributes { pub(super) mapped_name: Option, /// @map on enum values. - pub(super) mapped_values: HashMap, + pub(super) mapped_values: HashMap, /// ```ignore /// @@schema("public") /// ^^^^^^^^ diff --git a/psl/parser-database/src/walkers/composite_type.rs b/psl/parser-database/src/walkers/composite_type.rs index d6c93f125284..bf12e30d7c66 100644 --- a/psl/parser-database/src/walkers/composite_type.rs +++ b/psl/parser-database/src/walkers/composite_type.rs @@ -5,6 +5,8 @@ use crate::{ }; use diagnostics::Span; +use super::EnumWalker; + /// A composite type, introduced with the `type` keyword in the schema. /// /// Example: @@ -50,6 +52,11 @@ impl<'db> CompositeTypeWalker<'db> { self.ast_composite_type().name() } + /// Returns a specific field from the model. + pub fn field(&self, field_id: ast::FieldId) -> CompositeTypeFieldWalker<'db> { + self.walk((self.id, field_id)) + } + /// Iterator over all the fields of the composite type. pub fn fields(self) -> impl ExactSizeIterator> + Clone { self.ast_composite_type() @@ -111,6 +118,16 @@ impl<'db> CompositeTypeFieldWalker<'db> { self.ast_field().arity } + /// Is this field's type an enum? If yes, walk the enum. + pub fn field_type_as_enum(self) -> Option> { + self.r#type().as_enum().map(|id| self.db.walk(id)) + } + + /// Is this field's type a composite type? If yes, walk the composite type. + pub fn field_type_as_composite_type(self) -> Option> { + self.r#type().as_composite_type().map(|id| self.db.walk(id)) + } + /// The type of the field, e.g. `String` in `streetName String?`. pub fn r#type(self) -> ScalarFieldType { self.field().r#type diff --git a/psl/parser-database/src/walkers/enum.rs b/psl/parser-database/src/walkers/enum.rs index 8059ad73e5d3..659a713f1ed8 100644 --- a/psl/parser-database/src/walkers/enum.rs +++ b/psl/parser-database/src/walkers/enum.rs @@ -7,7 +7,7 @@ use crate::{ /// An `enum` declaration in the schema. pub type EnumWalker<'db> = Walker<'db, crate::EnumId>; /// One value in an `enum` declaration in the schema. -pub type EnumValueWalker<'db> = Walker<'db, (crate::EnumId, usize)>; +pub type EnumValueWalker<'db> = Walker<'db, (crate::EnumId, ast::EnumValueId)>; impl<'db> EnumWalker<'db> { fn attributes(self) -> &'db types::EnumAttributes { @@ -45,9 +45,16 @@ impl<'db> EnumWalker<'db> { self.attributes().mapped_name.map(|id| &self.db[id]) } + /// Returns the specific value from the model. + pub fn value(self, value_id: ast::EnumValueId) -> EnumValueWalker<'db> { + self.walk((self.id, value_id)) + } + /// The values of the enum. pub fn values(self) -> impl ExactSizeIterator> { - (0..self.ast_enum().values.len()).map(move |idx| self.walk((self.id, idx))) + self.ast_enum() + .iter_values() + .map(move |(value_id, _)| self.walk((self.id, value_id))) } /// How fields are indented in the enum. @@ -79,18 +86,19 @@ impl<'db> EnumWalker<'db> { } impl<'db> EnumValueWalker<'db> { - fn r#enum(self) -> EnumWalker<'db> { - self.walk(self.id.0) + /// The AST node. + pub fn ast_value(self) -> &'db ast::EnumValue { + &self.db.asts[self.id.0][self.id.1] } /// The enum documentation pub fn documentation(self) -> Option<&'db str> { - self.r#enum().ast_enum().values[self.id.1].documentation() + self.ast_value().documentation() } /// The name of the value. pub fn name(self) -> &'db str { - &self.r#enum().ast_enum().values[self.id.1].name.name + self.ast_value().name() } /// The database name of the enum. @@ -111,7 +119,7 @@ impl<'db> EnumValueWalker<'db> { pub fn mapped_name(self) -> Option<&'db str> { self.db.types.enum_attributes[&self.id.0] .mapped_values - .get(&(self.id.1 as u32)) + .get(&(self.id.1)) .map(|id| &self.db[*id]) } } diff --git a/psl/parser-database/src/walkers/field.rs b/psl/parser-database/src/walkers/field.rs index 87bea6560344..a27b094d4277 100644 --- a/psl/parser-database/src/walkers/field.rs +++ b/psl/parser-database/src/walkers/field.rs @@ -1,6 +1,6 @@ use super::{CompositeTypeFieldWalker, ModelWalker, RelationFieldWalker, ScalarFieldWalker, Walker}; use crate::{ - types::{RelationField, ScalarField}, + types::{RefinedFieldVariant, RelationField, ScalarField}, ScalarType, }; use schema_ast::ast; @@ -25,12 +25,20 @@ impl<'db> FieldWalker<'db> { } /// Find out which kind of field this is. - pub fn refine(self) -> RefinedFieldWalker<'db> { + /// Returns `None` if we encounter an unknown field. + pub fn refine(self) -> Option> { match self.db.types.refine_field(self.id) { - either::Either::Left(id) => RefinedFieldWalker::Relation(self.walk(id)), - either::Either::Right(id) => RefinedFieldWalker::Scalar(self.walk(id)), + RefinedFieldVariant::Relation(id) => Some(RefinedFieldWalker::Relation(self.walk(id))), + RefinedFieldVariant::Scalar(id) => Some(RefinedFieldWalker::Scalar(self.walk(id))), + RefinedFieldVariant::Unknown => None, } } + + /// Find out which kind of field this is. + /// ! Panics on unknown field, only to be used in query-engine where unknowns should not exist. + pub fn refine_known(self) -> RefinedFieldWalker<'db> { + self.refine().unwrap() + } } /// A field that has been identified as scalar field or relation field. diff --git a/psl/parser-database/src/walkers/model.rs b/psl/parser-database/src/walkers/model.rs index 088302095f3d..ea32bfe3acea 100644 --- a/psl/parser-database/src/walkers/model.rs +++ b/psl/parser-database/src/walkers/model.rs @@ -30,6 +30,11 @@ impl<'db> ModelWalker<'db> { self.id.0 } + /// Returns the specific field from the model. + pub fn field(&self, field_id: ast::FieldId) -> FieldWalker<'db> { + self.walk((self.id, field_id)) + } + /// Traverse the fields of the models in the order they were defined. pub fn fields(self) -> impl ExactSizeIterator> + Clone { self.ast_model() diff --git a/psl/parser-database/src/walkers/relation.rs b/psl/parser-database/src/walkers/relation.rs index 26e3ec61e052..6c017b1b2e2a 100644 --- a/psl/parser-database/src/walkers/relation.rs +++ b/psl/parser-database/src/walkers/relation.rs @@ -15,13 +15,13 @@ pub type RelationWalker<'db> = Walker<'db, RelationId>; impl<'db> RelationWalker<'db> { /// The models at each end of the relation. [model A, model B]. Can be the same model twice. pub fn models(self) -> [(FileId, ast::ModelId); 2] { - let rel = self.get(); + let rel = self.ast_relation(); [rel.model_a, rel.model_b] } /// The relation fields that define the relation. A then B. pub fn relation_fields(self) -> impl Iterator> { - let (a, b) = self.get().attributes.fields(); + let (a, b) = self.ast_relation().attributes.fields(); [a, b].into_iter().flatten().map(move |field| self.walk(field)) } @@ -38,16 +38,28 @@ impl<'db> RelationWalker<'db> { /// Is this a relation where both ends are the same model? pub fn is_self_relation(self) -> bool { - let r = self.get(); + let r = self.ast_relation(); r.model_a == r.model_b } + /// Gets relation kind + pub fn relation_kind(self) -> &'db str { + let r = self.ast_relation(); + + match r.attributes { + RelationAttributes::ImplicitManyToMany { .. } => "implicit many-to-many", + RelationAttributes::TwoWayEmbeddedManyToMany { .. } => "implicit many-to-many", + RelationAttributes::OneToOne(_) => "one-to-one", + RelationAttributes::OneToMany(_) => "one-to-many", + } + } + /// Converts the walker to either an implicit many to many, or a inline relation walker /// gathering 1:1 and 1:n relations. pub fn refine(self) -> RefinedRelationWalker<'db> { - if self.get().is_implicit_many_to_many() { + if self.ast_relation().is_implicit_many_to_many() { RefinedRelationWalker::ImplicitManyToMany(self.walk(ManyToManyRelationId(self.id))) - } else if self.get().is_two_way_embedded_many_to_many() { + } else if self.ast_relation().is_two_way_embedded_many_to_many() { RefinedRelationWalker::TwoWayEmbeddedManyToMany(TwoWayEmbeddedManyToManyRelationWalker(self)) } else { RefinedRelationWalker::Inline(InlineRelationWalker(self)) @@ -61,7 +73,7 @@ impl<'db> RelationWalker<'db> { /// // ^^^^^^^^^^^^^^^^^^^^^^^ /// ``` pub fn explicit_relation_name(self) -> Option<&'db str> { - self.get().relation_name.map(|string_id| &self.db[string_id]) + self.ast_relation().relation_name.map(|string_id| &self.db[string_id]) } /// The relation name, explicit or inferred. @@ -71,7 +83,7 @@ impl<'db> RelationWalker<'db> { /// ^^^^^^^^^^^ /// ``` pub fn relation_name(self) -> RelationName<'db> { - let relation = self.get(); + let relation = self.ast_relation(); relation .relation_name .map(|s| RelationName::Explicit(&self.db[s])) @@ -81,7 +93,7 @@ impl<'db> RelationWalker<'db> { } /// The relation attributes parsed from the AST. - fn get(self) -> &'db Relation { + fn ast_relation(self) -> &'db Relation { &self.db.relations[self.id] } } diff --git a/psl/parser-database/src/walkers/relation_field.rs b/psl/parser-database/src/walkers/relation_field.rs index 7f6b2e8037a4..5e387480d0b7 100644 --- a/psl/parser-database/src/walkers/relation_field.rs +++ b/psl/parser-database/src/walkers/relation_field.rs @@ -4,7 +4,11 @@ use crate::{ walkers::*, ReferentialAction, }; -use std::{borrow::Cow, fmt, hash::Hasher}; +use std::{ + borrow::Cow, + fmt::{self, Debug}, + hash::Hasher, +}; /// A relation field on a model in the schema. pub type RelationFieldWalker<'db> = Walker<'db, RelationFieldId>; @@ -97,7 +101,7 @@ impl<'db> RelationFieldWalker<'db> { self.db.walk(self.attributes().referenced_model) } - /// The fields in the `@relation(references: ...)` argument. + /// The fields in the `@relation(references: [...])` argument. pub fn referenced_fields(self) -> Option>> { self.attributes() .references @@ -154,7 +158,7 @@ impl<'db> RelationFieldWalker<'db> { self.fields() } - /// The fields in the `fields: [...]` argument in the forward relation field. + /// The fields in the `@relation(fields: [...])` argument in the forward relation field. pub fn fields(self) -> Option> + Clone> { let attributes = &self.db.types[self.id]; attributes diff --git a/psl/parser-database/src/walkers/scalar_field.rs b/psl/parser-database/src/walkers/scalar_field.rs index 7a9a0984584a..69b9add31d16 100644 --- a/psl/parser-database/src/walkers/scalar_field.rs +++ b/psl/parser-database/src/walkers/scalar_field.rs @@ -1,3 +1,5 @@ +use std::fmt; + use crate::{ ast::{self, WithName}, types::{DefaultAttribute, FieldWithArgs, OperatorClassStore, ScalarField, ScalarType, SortOrder}, @@ -108,6 +110,11 @@ impl<'db> ScalarFieldWalker<'db> { self.scalar_field_type().as_enum().map(|id| self.db.walk(id)) } + /// Is this field's type a composite type? If yes, walk the composite type. + pub fn field_type_as_composite_type(self) -> Option> { + self.scalar_field_type().as_composite_type().map(|id| self.db.walk(id)) + } + /// The name in the `@map()` attribute. pub fn mapped_name(self) -> Option<&'db str> { self.attributes().mapped_name.map(|id| &self.db[id]) @@ -158,6 +165,12 @@ impl<'db> ScalarFieldWalker<'db> { } } +impl<'db> fmt::Display for ScalarFieldWalker<'db> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name()) + } +} + /// An `@default()` attribute on a field. #[derive(Clone, Copy)] pub struct DefaultValueWalker<'db> { diff --git a/psl/psl/tests/attributes/composite_index.rs b/psl/psl/tests/attributes/composite_index.rs index ca13e4a32105..a98887b26eec 100644 --- a/psl/psl/tests/attributes/composite_index.rs +++ b/psl/psl/tests/attributes/composite_index.rs @@ -533,6 +533,12 @@ fn pointing_to_a_non_existing_type() { 16 |  id Int @id @map("_id") 17 |  a C  |  + error: Error validating model "B": The index definition refers to the relation fields a. Index definitions must reference only scalar fields. + --> schema.prisma:19 +  |  + 18 |  + 19 |  @@index([a.field]) +  |  "#]]; expected.assert_eq(&error); diff --git a/psl/psl/tests/base/basic.rs b/psl/psl/tests/base/basic.rs index b82a427be650..1f81fce3bbaa 100644 --- a/psl/psl/tests/base/basic.rs +++ b/psl/psl/tests/base/basic.rs @@ -235,6 +235,12 @@ fn type_aliases_must_error() {  |   1 | type MyString = String @default("B")  |  + error: Type "MyString" is neither a built-in type, nor refers to another model, composite type, or enum. + --> schema.prisma:5 +  |  +  4 |  id Int @id +  5 |  val MyString +  |  "#]]; expectation.assert_eq(&error); diff --git a/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma b/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma index 0fcd7f8a981c..f8b637573f80 100644 --- a/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma +++ b/psl/psl/tests/validation/composite_types/index_attributes_on_composite_types.prisma @@ -60,3 +60,15 @@ model B { // 15 |  @@index([other, field]) // 16 |  @@unique([content, rank]) //  |  +// error: Attribute not known: "@id". +// --> schema.prisma:7 +//  |  +//  6 | type A { +//  7 |  pk String @id +//  |  +// error: Attribute not known: "@unique". +// --> schema.prisma:8 +//  |  +//  7 |  pk String @id +//  8 |  field Int @unique +//  |  diff --git a/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma b/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma index 7d20fa73475e..2fc50789879b 100644 --- a/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma +++ b/psl/psl/tests/validation/composite_types/relation_field_attribute_not_allowed.prisma @@ -24,3 +24,9 @@ model B { // 11 |  c C[] @relation("foo") // 12 | } //  |  +// error: Attribute not known: "@relation". +// --> schema.prisma:11 +//  |  +// 10 | type A { +// 11 |  c C[] @relation("foo") +//  |  diff --git a/psl/schema-ast/src/ast/attribute.rs b/psl/schema-ast/src/ast/attribute.rs index fbf508bfa473..f664e4da2c5e 100644 --- a/psl/schema-ast/src/ast/attribute.rs +++ b/psl/schema-ast/src/ast/attribute.rs @@ -1,4 +1,4 @@ -use super::{ArgumentsList, Identifier, Span, WithIdentifier, WithSpan}; +use super::{ArgumentsList, EnumValueId, Identifier, Span, WithIdentifier, WithSpan}; use std::ops::Index; /// An attribute (following `@` or `@@``) on a model, model field, enum, enum value or composite @@ -51,7 +51,7 @@ pub enum AttributeContainer { Model(super::ModelId), ModelField(super::ModelId, super::FieldId), Enum(super::EnumId), - EnumValue(super::EnumId, u32), + EnumValue(super::EnumId, super::EnumValueId), CompositeTypeField(super::CompositeTypeId, super::FieldId), } @@ -79,8 +79,8 @@ impl From<(super::CompositeTypeId, super::FieldId)> for AttributeContainer { } } -impl From<(super::EnumId, u32)> for AttributeContainer { - fn from((enm, val): (super::EnumId, u32)) -> Self { +impl From<(super::EnumId, EnumValueId)> for AttributeContainer { + fn from((enm, val): (super::EnumId, super::EnumValueId)) -> Self { Self::EnumValue(enm, val) } } @@ -103,7 +103,7 @@ impl Index for super::SchemaAst { AttributeContainer::Model(model_id) => &self[model_id].attributes, AttributeContainer::ModelField(model_id, field_id) => &self[model_id][field_id].attributes, AttributeContainer::Enum(enum_id) => &self[enum_id].attributes, - AttributeContainer::EnumValue(enum_id, value_idx) => &self[enum_id].values[value_idx as usize].attributes, + AttributeContainer::EnumValue(enum_id, value_idx) => &self[enum_id][value_idx].attributes, AttributeContainer::CompositeTypeField(ctid, field_id) => &self[ctid][field_id].attributes, } } diff --git a/psl/schema-ast/src/ast/enum.rs b/psl/schema-ast/src/ast/enum.rs index 6ef4e1326c96..990e599d0e7f 100644 --- a/psl/schema-ast/src/ast/enum.rs +++ b/psl/schema-ast/src/ast/enum.rs @@ -112,6 +112,12 @@ impl WithDocumentation for Enum { pub struct EnumValue { /// The name of the enum value as it will be exposed by the api. pub name: Identifier, + /// The attributes of this value. + /// + /// ```ignore + /// yellow @map("orange") + /// ^^^^^^^^^^^^^^ + /// ``` pub attributes: Vec, pub(crate) documentation: Option, /// The location of this enum value in the text representation. diff --git a/psl/schema-ast/src/ast/field.rs b/psl/schema-ast/src/ast/field.rs index 3e355ecc2b41..394381a2f3b1 100644 --- a/psl/schema-ast/src/ast/field.rs +++ b/psl/schema-ast/src/ast/field.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + use super::{ Attribute, Comment, Identifier, Span, WithAttributes, WithDocumentation, WithIdentifier, WithName, WithSpan, }; @@ -40,6 +42,20 @@ pub struct Field { pub(crate) span: Span, } +impl Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let extension = if self.arity.is_list() { + "[]" + } else if self.arity.is_optional() { + "?" + } else { + "" + }; + + write!(f, "{} {}{}", self.name(), self.field_type.name(), extension) + } +} + impl Field { /// Finds the position span of the argument in the given field attribute. pub fn span_for_argument(&self, attribute: &str, argument: &str) -> Option { diff --git a/psl/schema-ast/src/ast/find_at_position.rs b/psl/schema-ast/src/ast/find_at_position.rs index b1a5c458bf79..c0201cf6c381 100644 --- a/psl/schema-ast/src/ast/find_at_position.rs +++ b/psl/schema-ast/src/ast/find_at_position.rs @@ -16,7 +16,7 @@ pub use field::FieldPosition; pub use generator::GeneratorPosition; pub use model::ModelPosition; pub use property::PropertyPosition; -pub use r#enum::EnumPosition; +pub use r#enum::{EnumPosition, EnumValuePosition}; use crate::ast::{self, top_idx_to_top_id, traits::*}; diff --git a/psl/schema-ast/src/ast/find_at_position/enum.rs b/psl/schema-ast/src/ast/find_at_position/enum.rs index 3138ef36e4c3..4749d4833a1b 100644 --- a/psl/schema-ast/src/ast/find_at_position/enum.rs +++ b/psl/schema-ast/src/ast/find_at_position/enum.rs @@ -63,6 +63,8 @@ impl<'ast> EnumPosition<'ast> { pub enum EnumValuePosition<'ast> { /// Nowhere specific inside the value Value, + /// In the name + Name(&'ast str), /// In an attribute. (name, idx, optional arg) /// In a value. /// ```prisma @@ -77,6 +79,9 @@ pub enum EnumValuePosition<'ast> { impl<'ast> EnumValuePosition<'ast> { fn new(value: &'ast ast::EnumValue, position: usize) -> EnumValuePosition<'ast> { + if value.name.span().contains(position) { + return EnumValuePosition::Name(value.name()); + } for (attr_idx, attr) in value.attributes.iter().enumerate() { if attr.span().contains(position) { // We can't go by Span::contains() because we also care about the empty space diff --git a/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs b/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs index c367695150f6..67b5417d4ab3 100644 --- a/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs +++ b/query-engine/dmmf/src/ast_builders/datamodel_ast_builder.rs @@ -148,14 +148,14 @@ fn model_to_dmmf(model: walkers::ModelWalker<'_>) -> Model { } fn should_skip_model_field(field: &walkers::FieldWalker<'_>) -> bool { - match field.refine() { + match field.refine_known() { walkers::RefinedFieldWalker::Scalar(f) => f.is_ignored() || f.is_unsupported(), walkers::RefinedFieldWalker::Relation(f) => f.is_ignored(), } } fn field_to_dmmf(field: walkers::FieldWalker<'_>) -> Field { - match field.refine() { + match field.refine_known() { walkers::RefinedFieldWalker::Scalar(sf) => scalar_field_to_dmmf(sf), walkers::RefinedFieldWalker::Relation(rf) => relation_field_to_dmmf(rf), }