diff --git a/prisma-fmt/src/get_dmmf.rs b/prisma-fmt/src/get_dmmf.rs index ebdffdd14747..02eec126d17d 100644 --- a/prisma-fmt/src/get_dmmf.rs +++ b/prisma-fmt/src/get_dmmf.rs @@ -106,7 +106,7 @@ mod tests { }); let expected = expect![[ - r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mThe `referentialIntegrity` and `relationMode` attributes cannot be used together. Please use only `relationMode` instead.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mschema.prisma:6\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 5 | \u001b[0m relationMode = \"prisma\"\n\u001b[1;94m 6 | \u001b[0m \u001b[1;91mreferentialIntegrity = \"foreignKeys\"\u001b[0m\n\u001b[1;94m 7 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"# + r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mThe `referentialIntegrity` and `relationMode` attributes cannot be used together. Please use only `relationMode` instead.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mschema.prisma:6\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 5 | \u001b[0m relationMode = \"prisma\"\n\u001b[1;94m 6 | \u001b[0m \u001b[1;91mreferentialIntegrity = \"foreignKeys\"\u001b[0m\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"# ]]; let response = get_dmmf(&request.to_string()).unwrap_err(); expected.assert_eq(&response); diff --git a/prisma-fmt/src/text_document_completion.rs b/prisma-fmt/src/text_document_completion.rs index 8f85cc8766ed..4df8f3e91471 100644 --- a/prisma-fmt/src/text_document_completion.rs +++ b/prisma-fmt/src/text_document_completion.rs @@ -12,6 +12,8 @@ use std::sync::Arc; use crate::position_to_offset; +mod datasource; + pub(crate) fn empty_completion_list() -> CompletionList { CompletionList { is_incomplete: true, @@ -119,7 +121,70 @@ fn push_ast_completions(ctx: CompletionContext<'_>, completion_list: &mut Comple push_namespaces(ctx, completion_list); } - position => ctx.connector().push_completions(ctx.db, position, completion_list), + ast::SchemaPosition::DataSource(_source_id, ast::SourcePosition::Source) => { + if !ds_has_prop(ctx, "provider") { + datasource::provider_completion(completion_list); + } + + if !ds_has_prop(ctx, "url") { + datasource::url_completion(completion_list); + } + + if !ds_has_prop(ctx, "shadowDatabaseUrl") { + datasource::shadow_db_completion(completion_list); + } + + if !ds_has_prop(ctx, "directUrl") { + datasource::direct_url_completion(completion_list); + } + + if !ds_has_prop(ctx, "relationMode") { + datasource::relation_mode_completion(completion_list); + } + + if let Some(config) = ctx.config { + ctx.connector().datasource_completions(config, completion_list); + } + } + + ast::SchemaPosition::DataSource( + _source_id, + ast::SourcePosition::Property("url", ast::PropertyPosition::FunctionValue("env")), + ) => datasource::url_env_db_completion(completion_list, "url", ctx), + + ast::SchemaPosition::DataSource( + _source_id, + ast::SourcePosition::Property("directUrl", ast::PropertyPosition::FunctionValue("env")), + ) => datasource::url_env_db_completion(completion_list, "directUrl", ctx), + + ast::SchemaPosition::DataSource( + _source_id, + ast::SourcePosition::Property("shadowDatabaseUrl", ast::PropertyPosition::FunctionValue("env")), + ) => datasource::url_env_db_completion(completion_list, "shadowDatabaseUrl", ctx), + + ast::SchemaPosition::DataSource(_source_id, ast::SourcePosition::Property("url", _)) + | ast::SchemaPosition::DataSource(_source_id, ast::SourcePosition::Property("directUrl", _)) + | ast::SchemaPosition::DataSource(_source_id, ast::SourcePosition::Property("shadowDatabaseUrl", _)) => { + datasource::url_env_completion(completion_list); + datasource::url_quotes_completion(completion_list); + } + + position => ctx.connector().datamodel_completions(ctx.db, position, completion_list), + } +} + +fn ds_has_prop(ctx: CompletionContext<'_>, prop: &str) -> bool { + if let Some(ds) = ctx.datasource() { + match prop { + "relationMode" => ds.relation_mode_defined(), + "directurl" => ds.direct_url_defined(), + "shadowDatabaseUrl" => ds.shadow_url_defined(), + "url" => ds.url_defined(), + "provider" => ds.provider_defined(), + _ => false, + } + } else { + false } } diff --git a/prisma-fmt/src/text_document_completion/datasource.rs b/prisma-fmt/src/text_document_completion/datasource.rs new file mode 100644 index 000000000000..2643e4e3630b --- /dev/null +++ b/prisma-fmt/src/text_document_completion/datasource.rs @@ -0,0 +1,160 @@ +use std::collections::HashMap; + +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionList, Documentation, InsertTextFormat, MarkupContent, MarkupKind, +}; +use psl::datamodel_connector::format_completion_docs; + +use super::{add_quotes, CompletionContext}; + +pub(super) fn relation_mode_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "relationMode".to_owned(), + insert_text: Some(r#"relationmode = $0"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"relationMode = "foreignKeys" | "prisma""#, + r#"Set the global relation mode for all relations. Values can be either "foreignKeys" (Default), or "prisma". [Learn more](https://pris.ly/d/relation-mode)"#, + None, + ), + })), + ..Default::default() + }) +} + +pub(super) fn direct_url_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "directUrl".to_owned(), + insert_text: Some(r#"directUrl = $0"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"directUrl = "String" | env("ENVIRONMENT_VARIABLE")"#, + r#"Connection URL for direct connection to the database. [Learn more](https://pris.ly/d/data-proxy-cli)."#, + None, + ) + })), + ..Default::default() + }) +} + +pub(super) fn shadow_db_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "shadowDatabaseUrl".to_owned(), + insert_text: Some(r#"shadowDatabaseUrl = $0"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"shadowDatabaseUrl = "String" | env("ENVIRONMENT_VARIABLE")"#, + r#"Connection URL including authentication info to use for Migrate's [shadow database](https://pris.ly/d/migrate-shadow)."#, + None, + ), + })), + ..Default::default() + }) +} + +pub(super) fn url_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "url".to_owned(), + insert_text: Some(r#"url = $0"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"url = "String" | env("ENVIRONMENT_VARIABLE")"#, + r#"Connection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/connection-strings)."#, + None, + ), + })), + ..Default::default() + }) +} + +pub(super) fn provider_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "provider".to_owned(), + insert_text: Some(r#"provider = $0"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"provider = "foo""#, + r#"Describes which datasource connector to use. Can be one of the following datasource providers: `postgresql`, `mysql`, `sqlserver`, `sqlite`, `mongodb` or `cockroachdb`."#, + None, + ), + })), + ..Default::default() + }) +} + +pub(super) fn url_env_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "env()".to_owned(), + insert_text: Some(r#"env($0)"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::PROPERTY), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"env(_ environmentVariable: string)"#, + r#"Specifies a datasource via an environment variable. When running a Prisma CLI command that needs the database connection URL (e.g. `prisma db pull`), you need to make sure that the `DATABASE_URL` environment variable is set. One way to do so is by creating a `.env` file. Note that the file must be in the same directory as your schema.prisma file to automatically be picked up by the Prisma CLI.""#, + Some(HashMap::from([( + "environmentVariable", + "The environment variable in which the database connection URL is stored.", + )])), + ), + })), + ..Default::default() + }) +} + +pub(super) fn url_quotes_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: r#""""#.to_owned(), + insert_text: Some(r#""$0""#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::PROPERTY), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#""connectionString""#, + r#"Connection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/prisma-schema)."#, + None, + ), + })), + ..Default::default() + }) +} + +pub(super) fn url_env_db_completion(completion_list: &mut CompletionList, kind: &str, ctx: CompletionContext<'_>) { + let text = match kind { + "url" => "DATABASE_URL", + "directUrl" => "DIRECT_URL", + "shadowDatabaseUrl" => "SHADOW_DATABASE_URL", + _ => unreachable!(), + }; + + let insert_text = if add_quotes(ctx.params, ctx.db.source()) { + format!(r#""{text}""#) + } else { + text.to_owned() + }; + + completion_list.items.push(CompletionItem { + label: text.to_owned(), + insert_text: Some(insert_text), + insert_text_format: Some(InsertTextFormat::PLAIN_TEXT), + kind: Some(CompletionItemKind::CONSTANT), + ..Default::default() + }) +} diff --git a/prisma-fmt/src/validate.rs b/prisma-fmt/src/validate.rs index 3fd50b3b0358..4cc9f88bf8bd 100644 --- a/prisma-fmt/src/validate.rs +++ b/prisma-fmt/src/validate.rs @@ -125,8 +125,9 @@ mod tests { }); let expected = expect![[ - r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mThe `referentialIntegrity` and `relationMode` attributes cannot be used together. Please use only `relationMode` instead.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mschema.prisma:6\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 5 | \u001b[0m relationMode = \"prisma\"\n\u001b[1;94m 6 | \u001b[0m \u001b[1;91mreferentialIntegrity = \"foreignKeys\"\u001b[0m\n\u001b[1;94m 7 | \u001b[0m }\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"# + r#"{"error_code":"P1012","message":"\u001b[1;91merror\u001b[0m: \u001b[1mThe `referentialIntegrity` and `relationMode` attributes cannot be used together. Please use only `relationMode` instead.\u001b[0m\n \u001b[1;94m-->\u001b[0m \u001b[4mschema.prisma:6\u001b[0m\n\u001b[1;94m | \u001b[0m\n\u001b[1;94m 5 | \u001b[0m relationMode = \"prisma\"\n\u001b[1;94m 6 | \u001b[0m \u001b[1;91mreferentialIntegrity = \"foreignKeys\"\u001b[0m\n\u001b[1;94m | \u001b[0m\n\nValidation Error Count: 1"}"# ]]; + let response = validate(&request.to_string()).unwrap_err(); expected.assert_eq(&response); } diff --git a/prisma-fmt/tests/code_actions/scenarios/relation_mode_referential_integrity/result.json b/prisma-fmt/tests/code_actions/scenarios/relation_mode_referential_integrity/result.json index bdd9ccd5214c..2b80fb1fb2f5 100644 --- a/prisma-fmt/tests/code_actions/scenarios/relation_mode_referential_integrity/result.json +++ b/prisma-fmt/tests/code_actions/scenarios/relation_mode_referential_integrity/result.json @@ -10,8 +10,8 @@ "character": 4 }, "end": { - "line": 4, - "character": 0 + "line": 3, + "character": 35 } }, "severity": 2, diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/result.json new file mode 100644 index 000000000000..4db6c65afbae --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/result.json @@ -0,0 +1,55 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "provider", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nprovider = \"foo\"\n```\n___\nDescribes which datasource connector to use. Can be one of the following datasource providers: `postgresql`, `mysql`, `sqlserver`, `sqlite`, `mongodb` or `cockroachdb`.\n\n" + }, + "insertText": "provider = $0", + "insertTextFormat": 2 + }, + { + "label": "url", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nurl = \"String\" | env(\"ENVIRONMENT_VARIABLE\")\n```\n___\nConnection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/connection-strings).\n\n" + }, + "insertText": "url = $0", + "insertTextFormat": 2 + }, + { + "label": "shadowDatabaseUrl", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nshadowDatabaseUrl = \"String\" | env(\"ENVIRONMENT_VARIABLE\")\n```\n___\nConnection URL including authentication info to use for Migrate's [shadow database](https://pris.ly/d/migrate-shadow).\n\n" + }, + "insertText": "shadowDatabaseUrl = $0", + "insertTextFormat": 2 + }, + { + "label": "directUrl", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\ndirectUrl = \"String\" | env(\"ENVIRONMENT_VARIABLE\")\n```\n___\nConnection URL for direct connection to the database. [Learn more](https://pris.ly/d/data-proxy-cli).\n\n" + }, + "insertText": "directUrl = $0", + "insertTextFormat": 2 + }, + { + "label": "relationMode", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nrelationMode = \"foreignKeys\" | \"prisma\"\n```\n___\nSet the global relation mode for all relations. Values can be either \"foreignKeys\" (Default), or \"prisma\". [Learn more](https://pris.ly/d/relation-mode)\n\n" + }, + "insertText": "relationmode = $0", + "insertTextFormat": 2 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/schema.prisma new file mode 100644 index 000000000000..d2d465ea61f2 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_default_completions/schema.prisma @@ -0,0 +1,7 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + <|> +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/result.json new file mode 100644 index 000000000000..d08a1920e27b --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/result.json @@ -0,0 +1,25 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "env()", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\nenv(_ environmentVariable: string)\n```\n___\nSpecifies a datasource via an environment variable. When running a Prisma CLI command that needs the database connection URL (e.g. `prisma db pull`), you need to make sure that the `DATABASE_URL` environment variable is set. One way to do so is by creating a `.env` file. Note that the file must be in the same directory as your schema.prisma file to automatically be picked up by the Prisma CLI.\"\n\n_@param_ environmentVariable The environment variable in which the database connection URL is stored." + }, + "insertText": "env($0)", + "insertTextFormat": 2 + }, + { + "label": "\"\"", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\n\"connectionString\"\n```\n___\nConnection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/prisma-schema).\n\n" + }, + "insertText": "\"$0\"", + "insertTextFormat": 2 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/schema.prisma new file mode 100644 index 000000000000..778cdf08cf6a --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_direct_url_arguments/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = "" + directUrl = <|> +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/result.json new file mode 100644 index 000000000000..084922869773 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/result.json @@ -0,0 +1,11 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "DIRECT_URL", + "kind": 21, + "insertText": "DIRECT_URL", + "insertTextFormat": 1 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/schema.prisma new file mode 100644 index 000000000000..652110c28013 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_direct_url/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = env("") + directUrl = env("<|>") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/result.json new file mode 100644 index 000000000000..03d4a6d3093a --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/result.json @@ -0,0 +1,11 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "SHADOW_DATABASE_URL", + "kind": 21, + "insertText": "SHADOW_DATABASE_URL", + "insertTextFormat": 1 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/schema.prisma new file mode 100644 index 000000000000..5abe6239d9e9 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_shadowdb_url/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = env("") + shadowDatabaseUrl = env("<|>") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/result.json new file mode 100644 index 000000000000..1561245640c5 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/result.json @@ -0,0 +1,11 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "DATABASE_URL", + "kind": 21, + "insertText": "DATABASE_URL", + "insertTextFormat": 1 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/schema.prisma new file mode 100644 index 000000000000..7f8ddf63cd58 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_env_db_url/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = env("<|>") +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/result.json new file mode 100644 index 000000000000..f4611e141cf9 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/result.json @@ -0,0 +1,45 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "shadowDatabaseUrl", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nshadowDatabaseUrl = \"String\" | env(\"ENVIRONMENT_VARIABLE\")\n```\n___\nConnection URL including authentication info to use for Migrate's [shadow database](https://pris.ly/d/migrate-shadow).\n\n" + }, + "insertText": "shadowDatabaseUrl = $0", + "insertTextFormat": 2 + }, + { + "label": "directUrl", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\ndirectUrl = \"String\" | env(\"ENVIRONMENT_VARIABLE\")\n```\n___\nConnection URL for direct connection to the database. [Learn more](https://pris.ly/d/data-proxy-cli).\n\n" + }, + "insertText": "directUrl = $0", + "insertTextFormat": 2 + }, + { + "label": "relationMode", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nrelationMode = \"foreignKeys\" | \"prisma\"\n```\n___\nSet the global relation mode for all relations. Values can be either \"foreignKeys\" (Default), or \"prisma\". [Learn more](https://pris.ly/d/relation-mode)\n\n" + }, + "insertText": "relationmode = $0", + "insertTextFormat": 2 + }, + { + "label": "schemas", + "kind": 5, + "documentation": { + "kind": "markdown", + "value": "```prisma\nschemas = [\"foo\", \"bar\", \"baz\"]\n```\n___\nThe list of database schemas. [Learn More](https://pris.ly/d/multi-schema-configuration)\n\n" + }, + "insertText": "schemas = [$0]", + "insertTextFormat": 2 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/schema.prisma new file mode 100644 index 000000000000..be18966c17fb --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_multischema/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + <|> +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/result.json new file mode 100644 index 000000000000..d08a1920e27b --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/result.json @@ -0,0 +1,25 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "env()", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\nenv(_ environmentVariable: string)\n```\n___\nSpecifies a datasource via an environment variable. When running a Prisma CLI command that needs the database connection URL (e.g. `prisma db pull`), you need to make sure that the `DATABASE_URL` environment variable is set. One way to do so is by creating a `.env` file. Note that the file must be in the same directory as your schema.prisma file to automatically be picked up by the Prisma CLI.\"\n\n_@param_ environmentVariable The environment variable in which the database connection URL is stored." + }, + "insertText": "env($0)", + "insertTextFormat": 2 + }, + { + "label": "\"\"", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\n\"connectionString\"\n```\n___\nConnection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/prisma-schema).\n\n" + }, + "insertText": "\"$0\"", + "insertTextFormat": 2 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/schema.prisma new file mode 100644 index 000000000000..760f67d50767 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_shadowdb_url_arguments/schema.prisma @@ -0,0 +1,10 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = "" + shadowDatabaseUrl = <|> +} diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/result.json b/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/result.json new file mode 100644 index 000000000000..d08a1920e27b --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/result.json @@ -0,0 +1,25 @@ +{ + "isIncomplete": false, + "items": [ + { + "label": "env()", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\nenv(_ environmentVariable: string)\n```\n___\nSpecifies a datasource via an environment variable. When running a Prisma CLI command that needs the database connection URL (e.g. `prisma db pull`), you need to make sure that the `DATABASE_URL` environment variable is set. One way to do so is by creating a `.env` file. Note that the file must be in the same directory as your schema.prisma file to automatically be picked up by the Prisma CLI.\"\n\n_@param_ environmentVariable The environment variable in which the database connection URL is stored." + }, + "insertText": "env($0)", + "insertTextFormat": 2 + }, + { + "label": "\"\"", + "kind": 10, + "documentation": { + "kind": "markdown", + "value": "```prisma\n\"connectionString\"\n```\n___\nConnection URL including authentication info. Each datasource provider documents the URL syntax. Most providers use the syntax provided by the database. [Learn more](https://pris.ly/d/prisma-schema).\n\n" + }, + "insertText": "\"$0\"", + "insertTextFormat": 2 + } + ] +} \ No newline at end of file diff --git a/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/schema.prisma b/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/schema.prisma new file mode 100644 index 000000000000..52d91e08b715 --- /dev/null +++ b/prisma-fmt/tests/text_document_completion/scenarios/datasource_url_arguments/schema.prisma @@ -0,0 +1,9 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["multischema"] +} + +datasource db { + provider = "postgresql" + url = <|> +} diff --git a/prisma-fmt/tests/text_document_completion/tests.rs b/prisma-fmt/tests/text_document_completion/tests.rs index b9eb1e5a905d..fa285babe4e9 100644 --- a/prisma-fmt/tests/text_document_completion/tests.rs +++ b/prisma-fmt/tests/text_document_completion/tests.rs @@ -36,4 +36,12 @@ scenarios! { referential_actions_middle_of_args_list referential_actions_mssql referential_actions_with_trailing_comma + datasource_default_completions + datasource_multischema + datasource_url_arguments + datasource_direct_url_arguments + datasource_shadowdb_url_arguments + datasource_env_db_url + datasource_env_db_direct_url + datasource_env_db_shadowdb_url } diff --git a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs index 11cd75d5e582..c477a8574d01 100644 --- a/psl/builtin-connectors/src/cockroach_datamodel_connector.rs +++ b/psl/builtin-connectors/src/cockroach_datamodel_connector.rs @@ -18,9 +18,12 @@ use psl_core::{ walkers::ModelWalker, IndexAlgorithm, ParserDatabase, ReferentialAction, ScalarType, }, + PreviewFeature, }; use std::borrow::Cow; +use crate::completions; + const CONSTRAINT_SCOPES: &[ConstraintScope] = &[ConstraintScope::ModelPrimaryKeyKeyIndexForeignKey]; const CAPABILITIES: &[ConnectorCapability] = &[ @@ -263,14 +266,19 @@ impl Connector for CockroachDatamodelConnector { Ok(()) } - fn push_completions(&self, _db: &ParserDatabase, position: SchemaPosition<'_>, completions: &mut CompletionList) { + fn datamodel_completions( + &self, + _db: &ParserDatabase, + position: SchemaPosition<'_>, + completion_list: &mut CompletionList, + ) { if let ast::SchemaPosition::Model( _, ast::ModelPosition::ModelAttribute("index", _, ast::AttributePosition::Argument("type")), ) = position { for index_type in self.supported_index_types() { - completions.items.push(CompletionItem { + completion_list.items.push(CompletionItem { label: index_type.to_string(), kind: Some(CompletionItemKind::ENUM), detail: Some(index_type.documentation().to_owned()), @@ -279,6 +287,17 @@ impl Connector for CockroachDatamodelConnector { } } } + + fn datasource_completions(&self, config: &psl_core::Configuration, completion_list: &mut CompletionList) { + let ds = match config.datasources.first() { + Some(ds) => ds, + None => return, + }; + + if config.preview_features().contains(PreviewFeature::MultiSchema) && !ds.schemas_defined() { + completions::schemas_completion(completion_list); + } + } } /// An `@default(sequence())` function. diff --git a/psl/builtin-connectors/src/completions.rs b/psl/builtin-connectors/src/completions.rs new file mode 100644 index 000000000000..a82db703910d --- /dev/null +++ b/psl/builtin-connectors/src/completions.rs @@ -0,0 +1,41 @@ +use lsp_types::{ + CompletionItem, CompletionItemKind, CompletionList, Documentation, InsertTextFormat, MarkupContent, MarkupKind, +}; +use psl_core::datamodel_connector::format_completion_docs; + +pub(crate) fn extensions_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "extensions".to_owned(), + insert_text: Some("extensions = [$0]".to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"extensions = [pg_trgm, postgis(version: "2.1")]"#, + r#"Enable PostgreSQL extensions. [Learn more](https://pris.ly/d/postgresql-extensions)"#, + None, + ), + })), + ..Default::default() + }) +} + +pub(crate) fn schemas_completion(completion_list: &mut CompletionList) { + completion_list.items.push(CompletionItem { + label: "schemas".to_owned(), + insert_text: Some(r#"schemas = [$0]"#.to_owned()), + insert_text_format: Some(InsertTextFormat::SNIPPET), + kind: Some(CompletionItemKind::FIELD), + documentation: Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: format_completion_docs( + r#"schemas = ["foo", "bar", "baz"]"#, + "The list of database schemas. [Learn More](https://pris.ly/d/multi-schema-configuration)", + None, + ), + })), + // detail: Some("schemas".to_owned()), + ..Default::default() + }); +} diff --git a/psl/builtin-connectors/src/lib.rs b/psl/builtin-connectors/src/lib.rs index f8d98f2f9821..dc1a33b1bc83 100644 --- a/psl/builtin-connectors/src/lib.rs +++ b/psl/builtin-connectors/src/lib.rs @@ -2,6 +2,7 @@ #![allow(clippy::derive_partial_eq_without_eq)] pub mod cockroach_datamodel_connector; +pub mod completions; pub use cockroach_datamodel_connector::CockroachType; pub use mongodb::MongoDbType; diff --git a/psl/builtin-connectors/src/mssql_datamodel_connector.rs b/psl/builtin-connectors/src/mssql_datamodel_connector.rs index fd0da4f981b1..7c5042599821 100644 --- a/psl/builtin-connectors/src/mssql_datamodel_connector.rs +++ b/psl/builtin-connectors/src/mssql_datamodel_connector.rs @@ -12,12 +12,15 @@ use psl_core::{ }, diagnostics::{Diagnostics, Span}, parser_database::{self, ast, ParserDatabase, ReferentialAction, ScalarType}, + PreviewFeature, }; use std::borrow::Cow; use MsSqlType::*; use MsSqlTypeParameter::*; +use crate::completions; + const CONSTRAINT_SCOPES: &[ConstraintScope] = &[ ConstraintScope::GlobalPrimaryKeyForeignKeyDefault, ConstraintScope::ModelPrimaryKeyKeyIndex, @@ -276,7 +279,7 @@ impl Connector for MsSqlDatamodelConnector { Ok(()) } - fn push_completions( + fn datamodel_completions( &self, _db: &ParserDatabase, position: ast::SchemaPosition<'_>, @@ -294,6 +297,17 @@ impl Connector for MsSqlDatamodelConnector { }); } } + + fn datasource_completions(&self, config: &psl_core::Configuration, completion_list: &mut CompletionList) { + let ds = match config.datasources.first() { + Some(ds) => ds, + None => return, + }; + + if config.preview_features().contains(PreviewFeature::MultiSchema) && !ds.schemas_defined() { + completions::schemas_completion(completion_list); + } + } } /// A collection of types stored outside of the row to the heap, having diff --git a/psl/builtin-connectors/src/mysql_datamodel_connector.rs b/psl/builtin-connectors/src/mysql_datamodel_connector.rs index fffcf1b43b26..edd639e80805 100644 --- a/psl/builtin-connectors/src/mysql_datamodel_connector.rs +++ b/psl/builtin-connectors/src/mysql_datamodel_connector.rs @@ -1,6 +1,7 @@ mod native_types; mod validations; +use lsp_types::CompletionList; pub use native_types::MySqlType; use enumflags2::BitFlags; @@ -10,9 +11,12 @@ use psl_core::{ }, diagnostics::{DatamodelError, Diagnostics, Span}, parser_database::{walkers, ReferentialAction, ScalarType}, + PreviewFeature, }; use MySqlType::*; +use crate::completions; + const TINY_BLOB_TYPE_NAME: &str = "TinyBlob"; const BLOB_TYPE_NAME: &str = "Blob"; const MEDIUM_BLOB_TYPE_NAME: &str = "MediumBlob"; @@ -259,4 +263,15 @@ impl Connector for MySqlDatamodelConnector { Ok(()) } + + fn datasource_completions(&self, config: &psl_core::Configuration, completion_list: &mut CompletionList) { + let ds = match config.datasources.first() { + Some(ds) => ds, + None => return, + }; + + if config.preview_features().contains(PreviewFeature::MultiSchema) && !ds.schemas_defined() { + completions::schemas_completion(completion_list); + } + } } diff --git a/psl/builtin-connectors/src/postgres_datamodel_connector.rs b/psl/builtin-connectors/src/postgres_datamodel_connector.rs index bd0c90fb5c85..07846d2bb30d 100644 --- a/psl/builtin-connectors/src/postgres_datamodel_connector.rs +++ b/psl/builtin-connectors/src/postgres_datamodel_connector.rs @@ -13,11 +13,13 @@ use psl_core::{ }, diagnostics::Diagnostics, parser_database::{ast, walkers, IndexAlgorithm, OperatorClass, ParserDatabase, ReferentialAction, ScalarType}, - Datasource, DatasourceConnectorData, PreviewFeature, + Configuration, Datasource, DatasourceConnectorData, PreviewFeature, }; use std::{borrow::Cow, collections::HashMap}; use PostgresType::*; +use crate::completions; + const CONSTRAINT_SCOPES: &[ConstraintScope] = &[ ConstraintScope::GlobalPrimaryKeyKeyIndex, ConstraintScope::ModelPrimaryKeyKeyIndexForeignKey, @@ -94,6 +96,11 @@ impl PostgresDatasourceProperties { span: ast::Span::empty(), }); } + + // Validation for property existence + pub fn extensions_defined(&self) -> bool { + self.extensions.is_some() + } } /// An extension defined in the extensions array of the datasource. @@ -443,7 +450,7 @@ impl Connector for PostgresDatamodelConnector { Ok(()) } - fn push_completions( + fn datamodel_completions( &self, db: &ParserDatabase, position: ast::SchemaPosition<'_>, @@ -518,6 +525,28 @@ impl Connector for PostgresDatamodelConnector { } } + fn datasource_completions(&self, config: &Configuration, completion_list: &mut CompletionList) { + let ds = match config.datasources.first() { + Some(ds) => ds, + None => return, + }; + + let connector_data = ds + .connector_data + .downcast_ref::() + .unwrap(); + + if config.preview_features().contains(PreviewFeature::PostgresqlExtensions) + && !connector_data.extensions_defined() + { + completions::extensions_completion(completion_list); + } + + if config.preview_features().contains(PreviewFeature::MultiSchema) && !ds.schemas_defined() { + completions::schemas_completion(completion_list); + } + } + fn parse_datasource_properties( &self, args: &mut HashMap<&str, (ast::Span, &ast::Expression)>, diff --git a/psl/diagnostics/src/error.rs b/psl/diagnostics/src/error.rs index 66da6b40c4eb..c6a16dffdba0 100644 --- a/psl/diagnostics/src/error.rs +++ b/psl/diagnostics/src/error.rs @@ -381,6 +381,16 @@ impl DatamodelError { Self::new(msg, span) } + pub fn new_config_property_missing_value_error( + property_name: &str, + config_name: &str, + config_kind: &str, + span: Span, + ) -> DatamodelError { + let msg = format!("Property {property_name} in {config_kind} {config_name} needs to be assigned a value"); + Self::new(msg, span) + } + pub fn span(&self) -> Span { self.span } diff --git a/psl/psl-core/src/configuration/datasource.rs b/psl/psl-core/src/configuration/datasource.rs index 0211d929119c..8bb97eae06c1 100644 --- a/psl/psl-core/src/configuration/datasource.rs +++ b/psl/psl-core/src/configuration/datasource.rs @@ -245,6 +245,31 @@ impl Datasource { Ok(Some(url)) } + + // Validation for property existence + pub fn provider_defined(&self) -> bool { + !self.provider.is_empty() + } + + pub fn url_defined(&self) -> bool { + self.url_span.end > self.url_span.start + } + + pub fn direct_url_defined(&self) -> bool { + self.direct_url.is_some() + } + + pub fn shadow_url_defined(&self) -> bool { + self.shadow_database_url.is_some() + } + + pub fn relation_mode_defined(&self) -> bool { + self.relation_mode.is_some() + } + + pub fn schemas_defined(&self) -> bool { + self.schemas_span.is_some() + } } pub(crate) fn from_url(url: &StringFromEnvVar, env: F) -> Result diff --git a/psl/psl-core/src/datamodel_connector.rs b/psl/psl-core/src/datamodel_connector.rs index a23b9dad7b6c..94888f143139 100644 --- a/psl/psl-core/src/datamodel_connector.rs +++ b/psl/psl-core/src/datamodel_connector.rs @@ -7,6 +7,9 @@ pub mod constraint_names; /// Extensions for parser database walkers with context from the connector. pub mod walker_ext_traits; +/// Connector completions +pub mod completions; + mod empty_connector; mod filters; mod native_types; @@ -14,13 +17,14 @@ mod relation_mode; pub use self::{ capabilities::{ConnectorCapabilities, ConnectorCapability}, + completions::format_completion_docs, empty_connector::EmptyDatamodelConnector, filters::*, native_types::{NativeTypeArguments, NativeTypeConstructor, NativeTypeInstance}, relation_mode::RelationMode, }; -use crate::{configuration::DatasourceConnectorData, Datasource, PreviewFeature}; +use crate::{configuration::DatasourceConnectorData, Configuration, Datasource, PreviewFeature}; use diagnostics::{DatamodelError, Diagnostics, NativeTypeErrorFactory, Span}; use enumflags2::BitFlags; use lsp_types::CompletionList; @@ -331,9 +335,16 @@ pub trait Connector: Send + Sync { fn validate_url(&self, url: &str) -> Result<(), String>; - fn push_completions(&self, _db: &ParserDatabase, _position: SchemaPosition<'_>, _completions: &mut CompletionList) { + fn datamodel_completions( + &self, + _db: &ParserDatabase, + _position: SchemaPosition<'_>, + _completions: &mut CompletionList, + ) { } + fn datasource_completions(&self, _config: &Configuration, _completion_list: &mut CompletionList) {} + fn parse_datasource_properties( &self, _args: &mut HashMap<&str, (Span, &ast::Expression)>, diff --git a/psl/psl-core/src/datamodel_connector/completions.rs b/psl/psl-core/src/datamodel_connector/completions.rs new file mode 100644 index 000000000000..08721917c41d --- /dev/null +++ b/psl/psl-core/src/datamodel_connector/completions.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +/// Formats the documentation for a completion. +/// example: How the completion is expected to be used. +/// +/// # Example +/// +/// ``` +/// use psl_core::datamodel_connector::format_completion_docs; +/// +/// let doc = format_completion_docs( +/// r#"relationMode = "foreignKeys" | "prisma""#, +/// r#"Sets the global relation mode for relations."#, +/// None, +/// ); +/// +/// assert_eq!( +/// "```prisma\nrelationMode = \"foreignKeys\" | \"prisma\"\n```\n___\nSets the global relation mode for relations.\n\n", +/// &doc +/// ); +/// ``` +pub fn format_completion_docs(example: &str, description: &str, params: Option>) -> String { + let param_docs: String = match params { + Some(params) => params + .into_iter() + .map(|(param_label, param_doc)| format!("_@param_ {param_label} {param_doc}")) + .collect::>() + .join("\n"), + None => Default::default(), + }; + + format!("```prisma\n{example}\n```\n___\n{description}\n\n{param_docs}") +} diff --git a/psl/psl-core/src/validate/datasource_loader.rs b/psl/psl-core/src/validate/datasource_loader.rs index 561e0454cba5..5b053edfe3cd 100644 --- a/psl/psl-core/src/validate/datasource_loader.rs +++ b/psl/psl-core/src/validate/datasource_loader.rs @@ -6,7 +6,10 @@ use crate::{ Datasource, }; use diagnostics::DatamodelWarning; -use parser_database::{ast::WithDocumentation, coerce, coerce_array, coerce_opt}; +use parser_database::{ + ast::{Expression, WithDocumentation}, + coerce, coerce_array, coerce_opt, +}; use std::{borrow::Cow, collections::HashMap}; const PREVIEW_FEATURES_KEY: &str = "previewFeatures"; @@ -14,6 +17,7 @@ const SCHEMAS_KEY: &str = "schemas"; const SHADOW_DATABASE_URL_KEY: &str = "shadowDatabaseUrl"; const URL_KEY: &str = "url"; const DIRECT_URL_KEY: &str = "directUrl"; +const PROVIDER_KEY: &str = "provider"; /// Loads all datasources from the provided schema AST. /// - `ignore_datasource_urls`: datasource URLs are not parsed. They are replaced with dummy values. @@ -49,85 +53,65 @@ fn lift_datasource( diagnostics: &mut Diagnostics, connectors: crate::ConnectorRegistry, ) -> Option { - let source_name = &ast_source.name.name; - let mut args: HashMap<_, _> = ast_source + let source_name = ast_source.name.name.as_str(); + let mut args: HashMap<_, (_, &Expression)> = ast_source .properties .iter() - .map(|arg| (arg.name.name.as_str(), (arg.span, &arg.value))) - .collect(); + .map(|arg| match &arg.value { + Some(expr) => Some((arg.name.name.as_str(), (arg.span, expr))), + None => { + diagnostics.push_error(DatamodelError::new_config_property_missing_value_error( + &arg.name.name, + source_name, + "datasource", + ast_source.span, + )); + None + } + }) + .collect::>>()?; - let (_, provider_arg) = match args.remove("provider") { - Some(provider) => provider, - None => { - diagnostics.push_error(DatamodelError::new_source_argument_not_found_error( - "provider", - &ast_source.name.name, - ast_source.span, - )); - return None; - } - }; + let (provider, provider_arg) = match args.remove(PROVIDER_KEY) { + Some((_span, provider_arg)) => { + if provider_arg.is_env_expression() { + let msg = Cow::Borrowed("A datasource must not use the env() function in the provider argument."); + diagnostics.push_error(DatamodelError::new_functional_evaluation_error(msg, ast_source.span)); + return None; + } - if provider_arg.is_env_expression() { - let msg = Cow::Borrowed("A datasource must not use the env() function in the provider argument."); - diagnostics.push_error(DatamodelError::new_functional_evaluation_error(msg, ast_source.span)); - return None; - } + let provider = match coerce_opt::string(provider_arg) { + Some("") => { + diagnostics.push_error(DatamodelError::new_source_validation_error( + "The provider argument in a datasource must not be empty", + source_name, + provider_arg.span(), + )); + return None; + } + None => { + diagnostics.push_error(DatamodelError::new_source_validation_error( + "The provider argument in a datasource must be a string literal", + source_name, + provider_arg.span(), + )); + return None; + } + Some(provider) => provider, + }; - let provider = match coerce_opt::string(provider_arg) { - Some("") => { - diagnostics.push_error(DatamodelError::new_source_validation_error( - "The provider argument in a datasource must not be empty", - source_name, - provider_arg.span(), - )); - return None; + (provider, provider_arg) } - None => { - diagnostics.push_error(DatamodelError::new_source_validation_error( - "The provider argument in a datasource must be a string literal", - source_name, - provider_arg.span(), - )); - return None; - } - Some(provider) => provider, - }; - let (_, url_arg) = match args.remove(URL_KEY) { - Some(url_arg) => url_arg, None => { diagnostics.push_error(DatamodelError::new_source_argument_not_found_error( - URL_KEY, - &ast_source.name.name, + "provider", + source_name, ast_source.span, )); return None; } }; - let url = StringFromEnvVar::coerce(url_arg, diagnostics)?; - let shadow_database_url_arg = args.remove(SHADOW_DATABASE_URL_KEY); - - let direct_url_arg = args.remove(DIRECT_URL_KEY).map(|(_, url)| url); - let direct_url = direct_url_arg.and_then(|url_arg| StringFromEnvVar::coerce(url_arg, diagnostics)); - - let shadow_database_url: Option<(StringFromEnvVar, Span)> = - if let Some((_, shadow_database_url_arg)) = shadow_database_url_arg.as_ref() { - match StringFromEnvVar::coerce(shadow_database_url_arg, diagnostics) { - Some(shadow_database_url) => Some(shadow_database_url) - .filter(|s| !s.as_literal().map(|lit| lit.is_empty()).unwrap_or(false)) - .map(|url| (url, shadow_database_url_arg.span())), - None => None, - } - } else { - None - }; - - preview_features_guardrail(&mut args, diagnostics); - - let documentation = ast_source.documentation().map(String::from); - let active_connector: &'static dyn crate::datamodel_connector::Connector = match connectors.iter().find(|c| c.is_provider(provider)) { Some(c) => *c, @@ -143,32 +127,73 @@ fn lift_datasource( let relation_mode = get_relation_mode(&mut args, ast_source, diagnostics, active_connector); - let (schemas, schemas_span) = args - .remove(SCHEMAS_KEY) - .and_then(|(_, expr)| coerce_array(expr, &coerce::string_with_span, diagnostics).map(|b| (b, expr.span()))) - .map(|(mut schemas, span)| { - if schemas.is_empty() { - let error = DatamodelError::new_schemas_array_empty_error(span); + let connector_data = active_connector.parse_datasource_properties(&mut args, diagnostics); - diagnostics.push_error(error); - } + let (url, url_span) = match args.remove(URL_KEY) { + Some((_span, url_arg)) => (StringFromEnvVar::coerce(url_arg, diagnostics)?, url_arg.span()), + + None => { + diagnostics.push_error(DatamodelError::new_source_argument_not_found_error( + URL_KEY, + source_name, + ast_source.span, + )); + + return None; + } + }; + + let shadow_database_url = match args.remove(SHADOW_DATABASE_URL_KEY) { + Some((_span, shadow_db_url_arg)) => match StringFromEnvVar::coerce(shadow_db_url_arg, diagnostics) { + Some(shadow_db_url) => Some(shadow_db_url) + .filter(|s| !s.as_literal().map(|literal| literal.is_empty()).unwrap_or(false)) + .map(|url| (url, shadow_db_url_arg.span())), + None => None, + }, + + _ => None, + }; + + let (direct_url, direct_url_span) = match args.remove(DIRECT_URL_KEY) { + Some((_, direct_url)) => ( + StringFromEnvVar::coerce(direct_url, diagnostics), + Some(direct_url.span()), + ), - schemas.sort_by(|(a, _), (b, _)| a.cmp(b)); + None => (None, None), + }; + + preview_features_guardrail(&mut args, diagnostics); + + let documentation = ast_source.documentation().map(String::from); - for pair in schemas.windows(2) { - if pair[0].0 == pair[1].0 { - diagnostics.push_error(DatamodelError::new_static( - "Duplicated schema names are not allowed", - pair[0].1, - )) + let (schemas, schemas_span) = match args.remove(SCHEMAS_KEY) { + Some((_span, schemas)) => coerce_array(schemas, &coerce::string_with_span, diagnostics) + .map(|b| (b, schemas.span())) + .and_then(|(mut schemas, span)| { + if schemas.is_empty() { + diagnostics.push_error(DatamodelError::new_schemas_array_empty_error(span)); + + return None; } - } - (schemas, Some(span)) - }) - .unwrap_or_default(); + schemas.sort_by(|(a, _), (b, _)| a.cmp(b)); - let connector_data = active_connector.parse_datasource_properties(&mut args, diagnostics); + for pair in schemas.windows(2) { + if pair[0].0 == pair[1].0 { + diagnostics.push_error(DatamodelError::new_static( + "Duplicated schema names are not allowed", + pair[0].1, + )) + } + } + + Some((schemas, Some(span))) + }) + .unwrap_or_default(), + + None => Default::default(), + }; for (name, (span, _)) in args.into_iter() { diagnostics.push_error(DatamodelError::new_property_not_known_error(name, span)); @@ -177,13 +202,13 @@ fn lift_datasource( Some(Datasource { namespaces: schemas.into_iter().map(|(s, span)| (s.to_owned(), span)).collect(), schemas_span, - name: source_name.to_string(), + name: source_name.to_owned(), provider: provider.to_owned(), active_provider: active_connector.provider_name(), url, - url_span: url_arg.span(), + url_span, direct_url, - direct_url_span: direct_url_arg.map(|arg| arg.span()), + direct_url_span, documentation, active_connector, shadow_database_url, diff --git a/psl/psl-core/src/validate/generator_loader.rs b/psl/psl-core/src/validate/generator_loader.rs index deae39ef8740..3118af266699 100644 --- a/psl/psl-core/src/validate/generator_loader.rs +++ b/psl/psl-core/src/validate/generator_loader.rs @@ -7,7 +7,7 @@ use crate::{ use enumflags2::BitFlags; use itertools::Itertools; use parser_database::{ - ast::{self, WithDocumentation}, + ast::{self, Expression, WithDocumentation}, coerce, coerce_array, }; use std::collections::HashMap; @@ -41,11 +41,24 @@ pub(crate) fn load_generators_from_ast(ast_schema: &ast::SchemaAst, diagnostics: } fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagnostics) -> Option { - let args: HashMap<_, _> = ast_generator + let generator_name = ast_generator.name.name.as_str(); + let args: HashMap<_, &Expression> = ast_generator .properties .iter() - .map(|arg| (arg.name.name.as_str(), &arg.value)) - .collect(); + .map(|arg| match &arg.value { + Some(expr) => Some((arg.name.name.as_str(), expr)), + None => { + diagnostics.push_error(DatamodelError::new_config_property_missing_value_error( + arg.name.name.as_str(), + generator_name, + "generator", + ast_generator.span, + )); + + None + } + }) + .collect::>>()?; if let Some(expr) = args.get(ENGINE_TYPE_KEY) { if !expr.is_string() { @@ -95,11 +108,23 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno } let value = match &prop.value { - ast::Expression::NumericValue(val, _) => val.clone(), - ast::Expression::StringValue(val, _) => val.clone(), - ast::Expression::ConstantValue(val, _) => val.clone(), - ast::Expression::Function(_, _, _) => String::from("(function)"), - ast::Expression::Array(_, _) => String::from("(array)"), + Some(val) => match val { + ast::Expression::NumericValue(val, _) => val.clone(), + ast::Expression::StringValue(val, _) => val.clone(), + ast::Expression::ConstantValue(val, _) => val.clone(), + ast::Expression::Function(_, _, _) => String::from("(function)"), + ast::Expression::Array(_, _) => String::from("(array)"), + }, + None => { + diagnostics.push_error(DatamodelError::new_config_property_missing_value_error( + &prop.name.name, + generator_name, + "generator", + prop.span, + )); + + continue; + } }; properties.insert(prop.name.name.clone(), value); diff --git a/psl/psl/tests/config/datasources.rs b/psl/psl/tests/config/datasources.rs index e3bd66561559..9ce2c3ea7db3 100644 --- a/psl/psl/tests/config/datasources.rs +++ b/psl/psl/tests/config/datasources.rs @@ -132,7 +132,6 @@ fn datasource_should_not_allow_arbitrary_parameters() {  |   3 |  url = "mysql://localhost"  4 |  foo = "bar" -  5 | }  |  "#]]; diff --git a/psl/psl/tests/config/sources.rs b/psl/psl/tests/config/sources.rs index 0cc0a9d50765..94bb5dcd96cc 100644 --- a/psl/psl/tests/config/sources.rs +++ b/psl/psl/tests/config/sources.rs @@ -648,7 +648,6 @@ fn fail_when_preview_features_are_declared() {  |   3 |  url = "mysql://"  4 |  previewFeatures = ["foo"] -  5 | }  |  "#]]; diff --git a/psl/psl/tests/validation/attributes/relation_mode/referential_integrity_attr_is_deprecated.prisma b/psl/psl/tests/validation/attributes/relation_mode/referential_integrity_attr_is_deprecated.prisma index 4b0c54357d41..711a0062af1b 100644 --- a/psl/psl/tests/validation/attributes/relation_mode/referential_integrity_attr_is_deprecated.prisma +++ b/psl/psl/tests/validation/attributes/relation_mode/referential_integrity_attr_is_deprecated.prisma @@ -8,5 +8,4 @@ datasource db { //  |  //  3 |  url = "sqlite" //  4 |  referentialIntegrity = "foreignKeys" -//  5 | } //  |  diff --git a/psl/psl/tests/validation/attributes/relation_mode/relation_mode_and_referential_integrity_cannot_cooccur.prisma b/psl/psl/tests/validation/attributes/relation_mode/relation_mode_and_referential_integrity_cannot_cooccur.prisma index 1b4066cd4c4f..ddd6b2bf9b12 100644 --- a/psl/psl/tests/validation/attributes/relation_mode/relation_mode_and_referential_integrity_cannot_cooccur.prisma +++ b/psl/psl/tests/validation/attributes/relation_mode/relation_mode_and_referential_integrity_cannot_cooccur.prisma @@ -9,12 +9,10 @@ datasource db { //  |  //  4 |  relationMode = "prisma" //  5 |  referentialIntegrity = "foreignKeys" -//  6 | } //  |  // error: The `referentialIntegrity` and `relationMode` attributes cannot be used together. Please use only `relationMode` instead. // --> schema.prisma:5 //  |  //  4 |  relationMode = "prisma" //  5 |  referentialIntegrity = "foreignKeys" -//  6 | } //  |  diff --git a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mongodb.prisma b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mongodb.prisma index 3deca2883f72..fe67e795ad21 100644 --- a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mongodb.prisma +++ b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mongodb.prisma @@ -13,5 +13,4 @@ datasource mypg { //  |  //  8 |  url = env("TEST_DATABASE_URL") //  9 |  extensions = [postgis] -// 10 | } //  |  diff --git a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mysql.prisma b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mysql.prisma index 6632ca3c20a7..972802b8adc7 100644 --- a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mysql.prisma +++ b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_mysql.prisma @@ -13,5 +13,4 @@ datasource db { //  |  //  8 |  url = env("TEST_DATABASE_URL") //  9 |  extensions = [postgis] -// 10 | } //  |  diff --git a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlite.prisma b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlite.prisma index 60c2190a28af..5316c0c2e05a 100644 --- a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlite.prisma +++ b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlite.prisma @@ -13,5 +13,4 @@ datasource db { //  |  //  8 |  url = env("TEST_DATABASE_URL") //  9 |  extensions = [postgis] -// 10 | } //  |  diff --git a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlserver.prisma b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlserver.prisma index 991703644551..cb0a6394a810 100644 --- a/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlserver.prisma +++ b/psl/psl/tests/validation/postgres_extensions/extensions_do_not_work_on_sqlserver.prisma @@ -13,5 +13,4 @@ datasource db { //  |  //  8 |  url = env("TEST_DATABASE_URL") //  9 |  extensions = [postgis] -// 10 | } //  |  diff --git a/psl/psl/tests/validation/postgres_extensions/extensions_require_feature_flag.prisma b/psl/psl/tests/validation/postgres_extensions/extensions_require_feature_flag.prisma index 7ff63caa2f0d..187a5ab292da 100644 --- a/psl/psl/tests/validation/postgres_extensions/extensions_require_feature_flag.prisma +++ b/psl/psl/tests/validation/postgres_extensions/extensions_require_feature_flag.prisma @@ -12,5 +12,4 @@ datasource mypg { //  |  //  7 |  url = env("TEST_DATABASE_URL") //  8 |  extensions = [ postgis ] -//  9 | } //  |  diff --git a/psl/schema-ast/src/ast.rs b/psl/schema-ast/src/ast.rs index 6c734d703144..a3380ea8b50a 100644 --- a/psl/schema-ast/src/ast.rs +++ b/psl/schema-ast/src/ast.rs @@ -107,10 +107,18 @@ impl std::ops::Index for SchemaAst { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct GeneratorId(u32); -/// An opaque identifier for a datasource blèck in a schema AST. +/// An opaque identifier for a datasource block in a schema AST. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct SourceId(u32); +impl std::ops::Index for SchemaAst { + type Output = SourceConfig; + + fn index(&self, index: SourceId) -> &Self::Output { + self.tops[index.0 as usize].as_source().unwrap() + } +} + /// An identifier for a top-level item in a schema AST. Use the `schema[top_id]` /// syntax to resolve the id to an `ast::Top`. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/psl/schema-ast/src/ast/composite_type.rs b/psl/schema-ast/src/ast/composite_type.rs index 4457c364085d..e0e42d405aca 100644 --- a/psl/schema-ast/src/ast/composite_type.rs +++ b/psl/schema-ast/src/ast/composite_type.rs @@ -37,6 +37,8 @@ pub struct CompositeType { pub(crate) documentation: Option, /// The location of this type in the text representation. pub span: Span, + /// The span of the inner contents. + pub inner_span: Span, } impl CompositeType { diff --git a/psl/schema-ast/src/ast/config.rs b/psl/schema-ast/src/ast/config.rs index 1bc04526f1de..d00ee5914910 100644 --- a/psl/schema-ast/src/ast/config.rs +++ b/psl/schema-ast/src/ast/config.rs @@ -27,7 +27,7 @@ pub struct ConfigBlockProperty { /// ^^^^^^^^^^ /// } /// ``` - pub value: Expression, + pub value: Option, /// The node span. pub span: Span, } diff --git a/psl/schema-ast/src/ast/enum.rs b/psl/schema-ast/src/ast/enum.rs index 0aa60ef7240d..33c7e3e8d230 100644 --- a/psl/schema-ast/src/ast/enum.rs +++ b/psl/schema-ast/src/ast/enum.rs @@ -70,6 +70,8 @@ pub struct Enum { pub(crate) documentation: Option, /// The location of this enum in the text representation. pub span: Span, + /// The span of the inner contents. + pub inner_span: Span, } impl Enum { diff --git a/psl/schema-ast/src/ast/find_at_position.rs b/psl/schema-ast/src/ast/find_at_position.rs index f8527a82a482..3cf597ebd2e5 100644 --- a/psl/schema-ast/src/ast/find_at_position.rs +++ b/psl/schema-ast/src/ast/find_at_position.rs @@ -9,6 +9,9 @@ impl ast::SchemaAst { SchemaPosition::Model(model_id, ModelPosition::new(&self[model_id], position)) } ast::TopId::Enum(enum_id) => SchemaPosition::Enum(enum_id, EnumPosition::new(&self[enum_id], position)), + ast::TopId::Source(source_id) => { + SchemaPosition::DataSource(source_id, SourcePosition::new(&self[source_id], position)) + } // Falling back to TopLevel as "not implemented" _ => SchemaPosition::TopLevel, }) @@ -45,6 +48,8 @@ pub enum SchemaPosition<'ast> { Model(ast::ModelId, ModelPosition<'ast>), /// In an enum Enum(ast::EnumId, EnumPosition<'ast>), + /// In a datasource + DataSource(ast::SourceId, SourcePosition<'ast>), } /// A cursor position in a context. @@ -318,3 +323,58 @@ impl<'ast> ExpressionPosition<'ast> { } } } + +#[derive(Debug)] +pub enum SourcePosition<'ast> { + /// In the general datasource + Source, + /// In a property + Property(&'ast str, PropertyPosition<'ast>), + /// Outside of the braces + Outer, +} + +impl<'ast> SourcePosition<'ast> { + fn new(source: &'ast ast::SourceConfig, position: usize) -> Self { + for property in &source.properties { + if property.span.contains(position) { + return SourcePosition::Property(&property.name.name, PropertyPosition::new(property, position)); + } + } + + if source.inner_span.contains(position) { + return SourcePosition::Source; + } + + SourcePosition::Outer + } +} + +#[derive(Debug)] +pub enum PropertyPosition<'ast> { + /// prop + Property, + /// + Value(&'ast str), + /// + FunctionValue(&'ast str), +} + +impl<'ast> PropertyPosition<'ast> { + fn new(property: &'ast ast::ConfigBlockProperty, position: usize) -> Self { + if let Some(val) = &property.value { + if val.span().contains(position) && val.is_function() { + let func = val.as_function().unwrap(); + + if func.0 == "env" { + return PropertyPosition::FunctionValue("env"); + } + } + } + if property.span.contains(position) && !property.name.span.contains(position) { + return PropertyPosition::Value(&property.name.name); + } + + PropertyPosition::Property + } +} diff --git a/psl/schema-ast/src/ast/source_config.rs b/psl/schema-ast/src/ast/source_config.rs index 3e38429388ef..fba385008c88 100644 --- a/psl/schema-ast/src/ast/source_config.rs +++ b/psl/schema-ast/src/ast/source_config.rs @@ -11,6 +11,8 @@ pub struct SourceConfig { pub(crate) documentation: Option, /// The location of this source block in the text representation. pub span: Span, + /// The span of the inner contents. + pub inner_span: Span, } impl WithIdentifier for SourceConfig { diff --git a/psl/schema-ast/src/parser/datamodel.pest b/psl/schema-ast/src/parser/datamodel.pest index 362895079d81..e0653b1f0780 100644 --- a/psl/schema-ast/src/parser/datamodel.pest +++ b/psl/schema-ast/src/parser/datamodel.pest @@ -30,7 +30,7 @@ model_declaration = { (MODEL_KEYWORD | TYPE_KEYWORD | VIEW_KEYWORD) ~ identifier ~ BLOCK_OPEN - ~ (field_declaration | (block_attribute ~ NEWLINE) | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* + ~ model_contents ~ BLOCK_CLOSE } @@ -43,6 +43,10 @@ field_declaration = { ~ NEWLINE } +model_contents = { + (field_declaration | (block_attribute ~ NEWLINE) | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* +} + // ###################################### // Field Type // ###################################### @@ -70,10 +74,15 @@ config_block = { (DATASOURCE_KEYWORD | GENERATOR_KEYWORD) ~ identifier ~ BLOCK_OPEN - ~ (key_value | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* + ~ config_contents ~ BLOCK_CLOSE } -key_value = { identifier ~ "=" ~ expression ~ trailing_comment? ~ NEWLINE } + +key_value = { identifier ~ "=" ~ expression? ~ trailing_comment? } + +config_contents = { + ((key_value ~ NEWLINE) | comment_block | empty_lines| BLOCK_LEVEL_CATCH_ALL)* +} // a block definition without a keyword. Is not valid. Just acts as a catch for the parser to display a nice error. arbitrary_block = { identifier ~ BLOCK_OPEN ~ ((!BLOCK_CLOSE ~ ANY) | NEWLINE)* ~ BLOCK_CLOSE } @@ -85,11 +94,14 @@ enum_declaration = { ENUM_KEYWORD ~ identifier ~ BLOCK_OPEN - ~ (enum_value_declaration | (block_attribute ~ NEWLINE) | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* + ~ enum_contents ~ BLOCK_CLOSE } enum_value_declaration = { identifier ~ field_attribute* ~ trailing_comment? ~ NEWLINE } +enum_contents = { + (enum_value_declaration | (block_attribute ~ NEWLINE) | comment_block | empty_lines | BLOCK_LEVEL_CATCH_ALL)* +} // ###################################### // Attributes diff --git a/psl/schema-ast/src/parser/parse_composite_type.rs b/psl/schema-ast/src/parser/parse_composite_type.rs index 63a6149ea4e5..8a42533dc945 100644 --- a/psl/schema-ast/src/parser/parse_composite_type.rs +++ b/psl/schema-ast/src/parser/parse_composite_type.rs @@ -6,7 +6,7 @@ use super::{ Rule, }; use crate::ast; -use diagnostics::{DatamodelError, Diagnostics}; +use diagnostics::{DatamodelError, Diagnostics, Span}; pub(crate) fn parse_composite_type( pair: Pair<'_>, @@ -16,97 +16,108 @@ pub(crate) fn parse_composite_type( let pair_span = pair.as_span(); let mut name: Option = None; let mut fields: Vec = vec![]; - let mut pending_field_comment: Option> = None; let mut pairs = pair.into_inner(); + let mut inner_span: Option = None; while let Some(current) = pairs.next() { - let current_span = current.as_span(); match current.as_rule() { + Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE => {} Rule::TYPE_KEYWORD => (), Rule::identifier => name = Some(current.into()), - Rule::block_attribute => { - let attr = parse_attribute(current, diagnostics); + Rule::model_contents => { + let mut pending_field_comment: Option> = None; + inner_span = Some(current.as_span().into()); - let err = match attr.name.name.as_str() { - "map" => { - DatamodelError::new_validation_error( - "The name of a composite type is not persisted in the database, therefore it does not need a mapped database name.", - current_span.into(), - ) - } - "unique" => { - DatamodelError::new_validation_error( - "A unique constraint should be defined in the model containing the embed.", - current_span.into(), - ) - } - "index" => { - DatamodelError::new_validation_error( - "An index should be defined in the model containing the embed.", - current_span.into(), - ) - } - "fulltext" => { - DatamodelError::new_validation_error( - "A fulltext index should be defined in the model containing the embed.", - current_span.into(), - ) - } - "id" => { - DatamodelError::new_validation_error( - "A composite type cannot define an id.", - current_span.into(), - ) - } - _ => { - DatamodelError::new_validation_error( - "A composite type cannot have block-level attributes.", - current_span.into(), - ) - } - }; + for item in current.into_inner() { + let current_span = item.as_span(); - diagnostics.push_error(err); - } - Rule::field_declaration => match parse_field( - &name.as_ref().unwrap().name, - "type", - current, - pending_field_comment.take(), - diagnostics, - ) { - Ok(field) => { - for attr in field.attributes.iter() { - let error = match attr.name.name.as_str() { - "relation" | "unique" | "id" => { - let name = attr.name.name.as_str(); + match item.as_rule() { + Rule::block_attribute => { + let attr = parse_attribute(item, diagnostics); - let msg = format!( - "Defining `@{name}` attribute for a field in a composite type is not allowed." - ); + let err = match attr.name.name.as_str() { + "map" => { + DatamodelError::new_validation_error( + "The name of a composite type is not persisted in the database, therefore it does not need a mapped database name.", + current_span.into(), + ) + } + "unique" => { + DatamodelError::new_validation_error( + "A unique constraint should be defined in the model containing the embed.", + current_span.into(), + ) + } + "index" => { + DatamodelError::new_validation_error( + "An index should be defined in the model containing the embed.", + current_span.into(), + ) + } + "fulltext" => { + DatamodelError::new_validation_error( + "A fulltext index should be defined in the model containing the embed.", + current_span.into(), + ) + } + "id" => { + DatamodelError::new_validation_error( + "A composite type cannot define an id.", + current_span.into(), + ) + } + _ => { + DatamodelError::new_validation_error( + "A composite type cannot have block-level attributes.", + current_span.into(), + ) + } + }; - DatamodelError::new_validation_error(&msg, current_span.into()) - } - _ => continue, - }; + diagnostics.push_error(err); + } + Rule::field_declaration => match parse_field( + &name.as_ref().unwrap().name, + "type", + item, + pending_field_comment.take(), + diagnostics, + ) { + Ok(field) => { + for attr in field.attributes.iter() { + let error = match attr.name.name.as_str() { + "relation" | "unique" | "id" => { + let name = attr.name.name.as_str(); - diagnostics.push_error(error); - } + let msg = format!( + "Defining `@{name}` attribute for a field in a composite type is not allowed." + ); - fields.push(field) - } - Err(err) => diagnostics.push_error(err), - }, - Rule::comment_block => { - if let Some(Rule::field_declaration) = pairs.peek().map(|p| p.as_rule()) { - pending_field_comment = Some(current); + DatamodelError::new_validation_error(&msg, current_span.into()) + } + _ => continue, + }; + + diagnostics.push_error(error); + } + + fields.push(field) + } + Err(err) => diagnostics.push_error(err), + }, + Rule::comment_block => { + if let Some(Rule::field_declaration) = pairs.peek().map(|p| p.as_rule()) { + pending_field_comment = Some(item); + } + } + Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( + "This line is not a valid field or attribute definition.", + item.as_span().into(), + )), + _ => parsing_catch_all(&item, "composite type"), + } } } - Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE => {} - Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( - "This line is not a valid field or attribute definition.", - current.as_span().into(), - )), _ => parsing_catch_all(¤t, "composite type"), } } @@ -117,6 +128,7 @@ pub(crate) fn parse_composite_type( fields, documentation: doc_comment.and_then(parse_comment_block), span: ast::Span::from(pair_span), + inner_span: inner_span.unwrap(), }, _ => panic!("Encountered impossible model declaration during parsing",), } diff --git a/psl/schema-ast/src/parser/parse_enum.rs b/psl/schema-ast/src/parser/parse_enum.rs index 3fdc8b7d4c70..04f300c6f799 100644 --- a/psl/schema-ast/src/parser/parse_enum.rs +++ b/psl/schema-ast/src/parser/parse_enum.rs @@ -13,29 +13,40 @@ pub fn parse_enum(pair: Pair<'_>, doc_comment: Option>, diagnostics: &m let mut name: Option = None; let mut attributes: Vec = vec![]; let mut values: Vec = vec![]; - let mut pending_value_comment = None; - let mut pairs = pair.into_inner().peekable(); + let pairs = pair.into_inner().peekable(); + let mut inner_span: Option = None; - while let Some(current) = pairs.next() { + for current in pairs { match current.as_rule() { Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE | Rule::ENUM_KEYWORD => {} Rule::identifier => name = Some(current.into()), - Rule::block_attribute => attributes.push(parse_attribute(current, diagnostics)), - Rule::enum_value_declaration => { - match parse_enum_value(current, pending_value_comment.take(), diagnostics) { - Ok(enum_value) => values.push(enum_value), - Err(err) => diagnostics.push_error(err), - } - } - Rule::comment_block => { - if let Some(Rule::enum_value_declaration) = pairs.peek().map(|t| t.as_rule()) { - pending_value_comment = Some(current); + Rule::enum_contents => { + let mut pending_value_comment = None; + inner_span = Some(current.as_span().into()); + + let mut items = current.into_inner(); + while let Some(item) = items.next() { + match item.as_rule() { + Rule::block_attribute => attributes.push(parse_attribute(item, diagnostics)), + Rule::enum_value_declaration => { + match parse_enum_value(item, pending_value_comment.take(), diagnostics) { + Ok(enum_value) => values.push(enum_value), + Err(err) => diagnostics.push_error(err), + } + } + Rule::comment_block => { + if let Some(Rule::enum_value_declaration) = items.peek().map(|t| t.as_rule()) { + pending_value_comment = Some(item); + } + } + Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( + "This line is not an enum value definition.", + item.as_span().into(), + )), + _ => parsing_catch_all(&item, "enum"), + } } } - Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( - "This line is not an enum value definition.", - current.as_span().into(), - )), _ => parsing_catch_all(¤t, "enum"), } } @@ -47,6 +58,7 @@ pub fn parse_enum(pair: Pair<'_>, doc_comment: Option>, diagnostics: &m attributes, documentation: comment, span: Span::from(pair_span), + inner_span: inner_span.unwrap(), }, _ => panic!("Encountered impossible enum declaration during parsing, name is missing.",), } diff --git a/psl/schema-ast/src/parser/parse_model.rs b/psl/schema-ast/src/parser/parse_model.rs index 79632b5cd520..f2aec884d61f 100644 --- a/psl/schema-ast/src/parser/parse_model.rs +++ b/psl/schema-ast/src/parser/parse_model.rs @@ -13,28 +13,36 @@ pub(crate) fn parse_model(pair: Pair<'_>, doc_comment: Option>, diagnos let mut name: Option = None; let mut attributes: Vec = Vec::new(); let mut fields: Vec = Vec::new(); - let mut pending_field_comment: Option> = None; for current in pair.into_inner() { match current.as_rule() { Rule::MODEL_KEYWORD | Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE => {} Rule::identifier => name = Some(current.into()), - Rule::block_attribute => attributes.push(parse_attribute(current, diagnostics)), - Rule::field_declaration => match parse_field( - &name.as_ref().unwrap().name, - "model", - current, - pending_field_comment.take(), - diagnostics, - ) { - Ok(field) => fields.push(field), - Err(err) => diagnostics.push_error(err), - }, - Rule::comment_block => pending_field_comment = Some(current), - Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( - "This line is not a valid field or attribute definition.", - current.as_span().into(), - )), + Rule::model_contents => { + let mut pending_field_comment: Option> = None; + + for item in current.into_inner() { + match item.as_rule() { + Rule::block_attribute => attributes.push(parse_attribute(item, diagnostics)), + Rule::field_declaration => match parse_field( + &name.as_ref().unwrap().name, + "model", + item, + pending_field_comment.take(), + diagnostics, + ) { + Ok(field) => fields.push(field), + Err(err) => diagnostics.push_error(err), + }, + Rule::comment_block => pending_field_comment = Some(item), + Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( + "This line is not a valid field or attribute definition.", + item.as_span().into(), + )), + _ => parsing_catch_all(&item, "model"), + } + } + } _ => parsing_catch_all(¤t, "model"), } } diff --git a/psl/schema-ast/src/parser/parse_source_and_generator.rs b/psl/schema-ast/src/parser/parse_source_and_generator.rs index 2c732417e848..d5abb6935fca 100644 --- a/psl/schema-ast/src/parser/parse_source_and_generator.rs +++ b/psl/schema-ast/src/parser/parse_source_and_generator.rs @@ -7,29 +7,40 @@ use super::{ use crate::ast::*; use diagnostics::{DatamodelError, Diagnostics}; +#[track_caller] pub(crate) fn parse_config_block(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> Top { let pair_span = pair.as_span(); let mut name: Option = None; let mut properties = Vec::new(); let mut comment: Option = None; let mut kw = None; + let mut inner_span: Option = None; for current in pair.into_inner() { match current.as_rule() { + Rule::config_contents => { + inner_span = Some(current.as_span().into()); + for item in current.into_inner() { + match item.as_rule() { + Rule::key_value => properties.push(parse_key_value(item, diagnostics)), + Rule::comment_block => comment = parse_comment_block(item), + Rule::BLOCK_LEVEL_CATCH_ALL => { + let msg = format!( + "This line is not a valid definition within a {}.", + kw.unwrap_or("configuration block") + ); + + let err = DatamodelError::new_validation_error(&msg, item.as_span().into()); + diagnostics.push_error(err); + } + _ => parsing_catch_all(&item, "source"), + } + } + } Rule::identifier => name = Some(current.into()), - Rule::key_value => properties.push(parse_key_value(current, diagnostics)), - Rule::comment_block => comment = parse_comment_block(current), Rule::DATASOURCE_KEYWORD | Rule::GENERATOR_KEYWORD => kw = Some(current.as_str()), Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE => {} - Rule::BLOCK_LEVEL_CATCH_ALL => { - let msg = format!( - "This line is not a valid definition within a {}.", - kw.unwrap_or("configuration block") - ); - let err = DatamodelError::new_validation_error(&msg, current.as_span().into()); - diagnostics.push_error(err); - } _ => parsing_catch_all(¤t, "source"), } } @@ -40,6 +51,7 @@ pub(crate) fn parse_config_block(pair: Pair<'_>, diagnostics: &mut Diagnostics) properties, documentation: comment, span: Span::from(pair_span), + inner_span: inner_span.unwrap(), }), Some("generator") => Top::Generator(GeneratorConfig { name: name.unwrap(), @@ -69,7 +81,7 @@ fn parse_key_value(pair: Pair<'_>, diagnostics: &mut Diagnostics) -> ConfigBlock } match (name, value) { - (Some(name), Some(value)) => ConfigBlockProperty { + (Some(name), value) => ConfigBlockProperty { name, value, span: Span::from(pair_span), diff --git a/psl/schema-ast/src/parser/parse_view.rs b/psl/schema-ast/src/parser/parse_view.rs index c58457aa9dc9..38066067b7a8 100644 --- a/psl/schema-ast/src/parser/parse_view.rs +++ b/psl/schema-ast/src/parser/parse_view.rs @@ -12,29 +12,37 @@ pub(crate) fn parse_view(pair: Pair<'_>, doc_comment: Option>, diagnost let pair_span = pair.as_span(); let mut name: Option = None; let mut fields: Vec = vec![]; - let mut pending_field_comment: Option> = None; let mut attributes: Vec = Vec::new(); for current in pair.into_inner() { match current.as_rule() { Rule::VIEW_KEYWORD | Rule::BLOCK_OPEN | Rule::BLOCK_CLOSE => (), Rule::identifier => name = Some(current.into()), - Rule::block_attribute => attributes.push(parse_attribute(current, diagnostics)), - Rule::field_declaration => match parse_field( - &name.as_ref().unwrap().name, - "view", - current, - pending_field_comment.take(), - diagnostics, - ) { - Ok(field) => fields.push(field), - Err(err) => diagnostics.push_error(err), - }, - Rule::comment_block => pending_field_comment = Some(current), - Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( - "This line is not a valid field or attribute definition.", - current.as_span().into(), - )), + Rule::model_contents => { + let mut pending_field_comment: Option> = None; + + for item in current.into_inner() { + match item.as_rule() { + Rule::block_attribute => attributes.push(parse_attribute(item, diagnostics)), + Rule::field_declaration => match parse_field( + &name.as_ref().unwrap().name, + "view", + item, + pending_field_comment.take(), + diagnostics, + ) { + Ok(field) => fields.push(field), + Err(err) => diagnostics.push_error(err), + }, + Rule::comment_block => pending_field_comment = Some(item), + Rule::BLOCK_LEVEL_CATCH_ALL => diagnostics.push_error(DatamodelError::new_validation_error( + "This line is not a valid field or attribute definition.", + item.as_span().into(), + )), + _ => parsing_catch_all(&item, "view"), + } + } + } _ => parsing_catch_all(¤t, "view"), } } diff --git a/psl/schema-ast/src/reformat.rs b/psl/schema-ast/src/reformat.rs index 759113944d28..853258226e2f 100644 --- a/psl/schema-ast/src/reformat.rs +++ b/psl/schema-ast/src/reformat.rs @@ -68,19 +68,60 @@ fn reformat_key_value(pair: Pair<'_>, table: &mut TableFormat) { } fn reformat_block_element(pair: Pair<'_>, renderer: &mut Renderer) { - let mut table = TableFormat::default(); - let mut attributes: Vec<(Option>, Pair<'_>)> = Vec::new(); // (Option, attribute) - let mut pending_block_comment = None; // comment before an attribute let mut pairs = pair.into_inner().peekable(); let block_type = pairs.next().unwrap().as_str(); loop { - let ate_empty_lines = eat_empty_lines(&mut pairs); + let current = match pairs.next() { + Some(current) => current, + None => return, + }; + + match current.as_rule() { + Rule::BLOCK_OPEN => { + // Reformat away the empty lines at the beginning of the block. + eat_empty_lines(&mut pairs); + } + Rule::BLOCK_CLOSE => {} + + Rule::model_contents | Rule::config_contents | Rule::enum_contents => { + reformat_block_contents(&mut current.into_inner().peekable(), renderer) + } + + Rule::identifier => { + let block_name = current.as_str(); + renderer.write(block_type); + renderer.write(" "); + renderer.write(block_name); + renderer.write(" {"); + renderer.end_line(); + renderer.indent_up(); + } + + _ => unreachable(¤t), + } + } +} + +fn reformat_block_contents<'a>( + pairs: &mut Peekable>>, + renderer: &mut Renderer, +) { + let mut attributes: Vec<(Option>, Pair<'_>)> = Vec::new(); // (Option, attribute) + let mut table = TableFormat::default(); + + let mut pending_block_comment = None; // comment before an attribute + + // Reformat away the empty lines at the beginning of the block. + eat_empty_lines(pairs); + + loop { + let ate_empty_lines = eat_empty_lines(pairs); // Decide what to do with the empty lines. if ate_empty_lines { match pairs.peek().map(|pair| pair.as_rule()) { - Some(Rule::BLOCK_CLOSE) | Some(Rule::block_attribute) | Some(Rule::comment_block) => { + None | Some(Rule::block_attribute) | Some(Rule::comment_block) => { // Reformat away the empty lines at the end of blocks and before attributes (we // re-add them later). } @@ -90,21 +131,11 @@ fn reformat_block_element(pair: Pair<'_>, renderer: &mut Renderer) { table = TableFormat::default(); table.start_new_line(); } - _ => (), } } - let current = match pairs.next() { Some(current) => current, - None => return, - }; - - match current.as_rule() { - Rule::BLOCK_OPEN => { - // Reformat away the empty lines at the beginning of the block. - eat_empty_lines(&mut pairs); - } - Rule::BLOCK_CLOSE => { + None => { // Flush current table. table.render(renderer); table = Default::default(); @@ -127,18 +158,11 @@ fn reformat_block_element(pair: Pair<'_>, renderer: &mut Renderer) { renderer.indent_down(); renderer.write("}"); renderer.end_line(); + return; } + }; - Rule::identifier => { - let block_name = current.as_str(); - renderer.write(block_type); - renderer.write(" "); - renderer.write(block_name); - renderer.write(" {"); - renderer.end_line(); - renderer.indent_up(); - } - + match current.as_rule() { Rule::comment_block => { if pairs.peek().map(|pair| pair.as_rule()) == Some(Rule::block_attribute) { pending_block_comment = Some(current.clone()); // move it with the attribute