diff --git a/psl/diagnostics/src/warning.rs b/psl/diagnostics/src/warning.rs index bf6b9b8116d3..fa5dfad0d4bf 100644 --- a/psl/diagnostics/src/warning.rs +++ b/psl/diagnostics/src/warning.rs @@ -4,6 +4,7 @@ use crate::{ }; use colored::{ColoredString, Colorize}; use indoc::indoc; +use std::fmt::Display; /// A non-fatal warning emitted by the schema parser. /// For fancy printing, please use the `pretty_print_error` function. @@ -20,13 +21,38 @@ impl DatamodelWarning { DatamodelWarning { message, span } } - pub fn new_feature_deprecated(feature: &str, span: Span) -> DatamodelWarning { + pub fn new_preview_feature_deprecated(feature: &str, span: Span) -> DatamodelWarning { let message = format!( "Preview feature \"{feature}\" is deprecated. The functionality can be used without specifying it as a preview feature." ); Self::new(message, span) } + pub fn new_preview_feature_renamed( + deprecated_feature: &str, + renamed_feature: impl Display, + prisly_link_endpoint: &str, + span: Span, + ) -> DatamodelWarning { + let message = format!( + "Preview feature \"{deprecated_feature}\" has been renamed as \"{renamed_feature}\". Learn more at https://pris.ly/d/{prisly_link_endpoint}." + ); + Self::new(message, span) + } + + pub fn new_preview_feature_renamed_for_provider( + provider: &'static str, + deprecated_feature: &str, + renamed_feature: impl Display, + prisly_link_endpoint: &str, + span: Span, + ) -> DatamodelWarning { + let message = format!( + "On `provider = \"{provider}\"`, preview feature \"{deprecated_feature}\" has been renamed as \"{renamed_feature}\". Learn more at https://pris.ly/d/{prisly_link_endpoint}." + ); + Self::new(message, span) + } + pub fn new_referential_integrity_attr_deprecation_warning(span: Span) -> DatamodelWarning { let message = "The `referentialIntegrity` attribute is deprecated. Please use `relationMode` instead. Learn more at https://pris.ly/d/relation-mode"; Self::new(message.to_string(), span) diff --git a/psl/psl-core/src/common.rs b/psl/psl-core/src/common.rs index 1734ce6619a5..8c957ac48674 100644 --- a/psl/psl-core/src/common.rs +++ b/psl/psl-core/src/common.rs @@ -2,4 +2,5 @@ mod preview_features; -pub use self::preview_features::{FeatureMap, PreviewFeature, PreviewFeatures, ALL_PREVIEW_FEATURES}; +pub(crate) use self::preview_features::RenamedFeature; +pub use self::preview_features::{FeatureMapWithProvider, PreviewFeature, PreviewFeatures, ALL_PREVIEW_FEATURES}; diff --git a/psl/psl-core/src/common/preview_features.rs b/psl/psl-core/src/common/preview_features.rs index 1584c0e42dc0..c43ddd2cdfdc 100644 --- a/psl/psl-core/src/common/preview_features.rs +++ b/psl/psl-core/src/common/preview_features.rs @@ -1,5 +1,7 @@ use serde::{Serialize, Serializer}; +use std::collections::BTreeMap; use std::fmt; +use std::sync::LazyLock; /// A set of preview features. pub type PreviewFeatures = enumflags2::BitFlags; @@ -8,7 +10,7 @@ macro_rules! features { ($( $variant:ident $(,)? ),*) => { #[enumflags2::bitflags] #[repr(u64)] - #[derive(Debug, Copy, Clone, PartialEq, Eq)] + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum PreviewFeature { $( $variant,)* } @@ -86,88 +88,192 @@ features!( StrictUndefinedChecks ); -/// Generator preview features (alphabetically sorted) -pub const ALL_PREVIEW_FEATURES: FeatureMap = FeatureMap { - active: enumflags2::make_bitflags!(PreviewFeature::{ - Deno - | DriverAdapters - | Metrics - | MultiSchema - | NativeDistinct - | PostgresqlExtensions - | Tracing - | Views - | RelationJoins - | OmitApi - | PrismaSchemaFolder - | StrictUndefinedChecks - }), - deprecated: enumflags2::make_bitflags!(PreviewFeature::{ - AtomicNumberOperations - | AggregateApi - | ClientExtensions - | Cockroachdb - | ConnectOrCreate - | CreateMany - | DataProxy - | Distinct - | ExtendedIndexes - | ExtendedWhereUnique - | FieldReference - | FilteredRelationCount - | FilterJson - | FullTextIndex - | FullTextSearch - | GroupBy - | ImprovedQueryRaw - | InteractiveTransactions - | JsonProtocol - | MicrosoftSqlServer - | Middlewares - | MongoDb - | NamedConstraints - | NApi - | NativeTypes - | OrderByAggregateGroup - | OrderByNulls - | OrderByRelation - | ReferentialActions - | ReferentialIntegrity - | SelectRelationCount - | TransactionApi - | UncheckedScalarInputs - }), - hidden: enumflags2::make_bitflags!(PreviewFeature::{ReactNative | TypedSql | NativeFullTextSearchPostgres}), -}; - -#[derive(Debug)] +#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +struct RenamedFeatureKey { + /// The old, deprecated preview feature that was renamed. + pub from: PreviewFeature, + + /// The provider that the feature was renamed for. + pub provider: Option<&'static str>, +} + +#[derive(Debug, Copy, Clone)] +pub(crate) struct RenamedFeatureValue { + /// The new preview feature. + pub to: PreviewFeature, + + /// The Pris.ly link endpoint for the feature, i.e., what comes after `https://pris.ly/d/`. + pub prisly_link_endpoint: &'static str, +} + +#[derive(Debug, Clone)] +pub(crate) enum RenamedFeature { + /// The preview feature was renamed for a specific provider. + ForProvider((&'static str, RenamedFeatureValue)), + + /// The preview feature was renamed for all providers. + AllProviders(RenamedFeatureValue), +} + +#[derive(Debug, Clone)] pub struct FeatureMap { /// Valid, visible features. active: PreviewFeatures, + /// Valid, but connector-specific features that are only visible on matching provider key. + native: BTreeMap<&'static str, PreviewFeatures>, + /// Deprecated features. deprecated: PreviewFeatures, + /// History of renamed deprecated features. + renamed: BTreeMap, + /// Hidden preview features are valid features, but are not propagated into the tooling /// (as autocomplete or similar) or into error messages (eg. showing a list of valid features). hidden: PreviewFeatures, } -impl FeatureMap { - pub const fn active_features(&self) -> PreviewFeatures { - self.active +#[derive(Debug, Clone)] +pub struct FeatureMapWithProvider { + provider: Option<&'static str>, + feature_map: FeatureMap, +} + +/// The default feature map with an unknown provider. +/// This is used for convenience in `prisma/language-tools`, which needs the list of all available preview features +/// before a provider is necessarily known. +pub static ALL_PREVIEW_FEATURES: LazyLock = LazyLock::new(|| FeatureMapWithProvider::new(None)); + +impl FeatureMapWithProvider { + pub fn new(connector_provider: Option<&'static str>) -> FeatureMapWithProvider { + // Generator preview features (alphabetically sorted) + let feature_map: FeatureMap = FeatureMap { + active: enumflags2::make_bitflags!(PreviewFeature::{ + Deno + | DriverAdapters + | Metrics + | MultiSchema + | NativeDistinct + | OmitApi + | PostgresqlExtensions + | PrismaSchemaFolder + | RelationJoins + | StrictUndefinedChecks + | Tracing + | Views + }), + native: BTreeMap::from([ + #[cfg(feature = "postgresql")] + ( + "postgresql", + enumflags2::make_bitflags!(PreviewFeature::{ + NativeFullTextSearchPostgres + }), + ), + ]), + renamed: BTreeMap::from([ + #[cfg(feature = "postgresql")] + ( + RenamedFeatureKey { + from: PreviewFeature::FullTextSearch, + provider: Some("postgresql"), + }, + RenamedFeatureValue { + to: PreviewFeature::NativeFullTextSearchPostgres, + prisly_link_endpoint: "native-fts-postgres", + }, + ), + ]), + deprecated: enumflags2::make_bitflags!(PreviewFeature::{ + AtomicNumberOperations + | AggregateApi + | ClientExtensions + | Cockroachdb + | ConnectOrCreate + | CreateMany + | DataProxy + | Distinct + | ExtendedIndexes + | ExtendedWhereUnique + | FieldReference + | FilteredRelationCount + | FilterJson + | FullTextIndex + | FullTextSearch + | GroupBy + | ImprovedQueryRaw + | InteractiveTransactions + | JsonProtocol + | MicrosoftSqlServer + | Middlewares + | MongoDb + | NamedConstraints + | NApi + | NativeTypes + | OrderByAggregateGroup + | OrderByNulls + | OrderByRelation + | ReferentialActions + | ReferentialIntegrity + | SelectRelationCount + | TransactionApi + | UncheckedScalarInputs + }), + hidden: enumflags2::make_bitflags!(PreviewFeature::{ReactNative | TypedSql}), + }; + + Self { + provider: connector_provider, + feature_map, + } + } + + pub fn native_features(&self) -> PreviewFeatures { + self.provider + .and_then(|provider| self.feature_map.native.get(provider).copied()) + .unwrap_or_default() + } + + pub fn active_features(&self) -> PreviewFeatures { + self.feature_map.active | self.native_features() } pub const fn hidden_features(&self) -> PreviewFeatures { - self.hidden + self.feature_map.hidden } pub(crate) fn is_valid(&self, flag: PreviewFeature) -> bool { - (self.active | self.hidden).contains(flag) + (self.active_features() | self.feature_map.hidden).contains(flag) } pub(crate) fn is_deprecated(&self, flag: PreviewFeature) -> bool { - self.deprecated.contains(flag) + self.feature_map.deprecated.contains(flag) + } + + /// Was the given preview feature deprecated and renamed? + pub(crate) fn is_renamed<'f>(&self, flag: PreviewFeature) -> Option { + // Check for a renamed feature specific to the provider. This is only possible if a provider is not None. + let provider_specific = self.provider.and_then(|provider| { + self.feature_map + .renamed + .get(&RenamedFeatureKey { + from: flag, + provider: Some(provider), + }) + .map(|renamed| RenamedFeature::ForProvider((provider, renamed.clone()))) + }); + + // Fallback to provider-independent renamed feature + provider_specific.or_else(|| { + self.feature_map + .renamed + .get(&RenamedFeatureKey { + from: flag, + provider: None, + }) + .map(|renamed| RenamedFeature::AllProviders(renamed.clone())) + }) } } diff --git a/psl/psl-core/src/lib.rs b/psl/psl-core/src/lib.rs index e4b36dd15e92..150958ff43e0 100644 --- a/psl/psl-core/src/lib.rs +++ b/psl/psl-core/src/lib.rs @@ -18,7 +18,7 @@ mod validate; use std::sync::Arc; pub use crate::{ - common::{PreviewFeature, PreviewFeatures, ALL_PREVIEW_FEATURES}, + common::{FeatureMapWithProvider, PreviewFeature, PreviewFeatures, ALL_PREVIEW_FEATURES}, configuration::{ Configuration, Datasource, DatasourceConnectorData, Generator, GeneratorConfigValue, StringFromEnvVar, }, @@ -171,8 +171,18 @@ fn validate_configuration( diagnostics: &mut Diagnostics, connectors: ConnectorRegistry<'_>, ) -> Configuration { - let generators = generator_loader::load_generators_from_ast(schema_ast, diagnostics); let datasources = datasource_loader::load_datasources_from_ast(schema_ast, diagnostics, connectors); + // We need to know the active provider to determine which features are active. + // This was originally introduced because the `fullTextSearch` preview feature will hit GA stage + // one connector at a time (Prisma 6 GAs it for MySQL, other connectors may follow in future releases). + let feature_map_with_provider: FeatureMapWithProvider = datasources + .first() + .map(|ds| Some(ds.active_provider)) + .map(|provider| FeatureMapWithProvider::new(provider)) + .unwrap_or_else(|| (*ALL_PREVIEW_FEATURES).clone()); + + let generators = generator_loader::load_generators_from_ast(schema_ast, diagnostics, &feature_map_with_provider); + Configuration::new(generators, datasources, diagnostics.warnings().to_owned()) } diff --git a/psl/psl-core/src/validate/generator_loader.rs b/psl/psl-core/src/validate/generator_loader.rs index ecd1ae1975c1..cb62d36d4bb6 100644 --- a/psl/psl-core/src/validate/generator_loader.rs +++ b/psl/psl-core/src/validate/generator_loader.rs @@ -1,6 +1,6 @@ use crate::{ ast::WithSpan, - common::{FeatureMap, PreviewFeature, ALL_PREVIEW_FEATURES}, + common::{FeatureMapWithProvider, PreviewFeature, RenamedFeature}, configuration::{Generator, GeneratorConfigValue, StringFromEnvVar}, diagnostics::*, }; @@ -22,11 +22,15 @@ const ENGINE_TYPE_KEY: &str = "engineType"; const FIRST_CLASS_PROPERTIES: &[&str] = &[PROVIDER_KEY, OUTPUT_KEY, BINARY_TARGETS_KEY, PREVIEW_FEATURES_KEY]; /// Load and validate Generators defined in an AST. -pub(crate) fn load_generators_from_ast(ast_schema: &ast::SchemaAst, diagnostics: &mut Diagnostics) -> Vec { +pub(crate) fn load_generators_from_ast( + ast_schema: &ast::SchemaAst, + diagnostics: &mut Diagnostics, + feature_map_with_provider: &FeatureMapWithProvider, +) -> Vec { let mut generators: Vec = Vec::new(); for gen in ast_schema.generators() { - if let Some(generator) = lift_generator(gen, diagnostics) { + if let Some(generator) = lift_generator(gen, diagnostics, feature_map_with_provider) { generators.push(generator); } } @@ -34,7 +38,11 @@ pub(crate) fn load_generators_from_ast(ast_schema: &ast::SchemaAst, diagnostics: generators } -fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagnostics) -> Option { +fn lift_generator( + ast_generator: &ast::GeneratorConfig, + diagnostics: &mut Diagnostics, + feature_map_with_provider: &FeatureMapWithProvider, +) -> Option { let generator_name = ast_generator.name.name.as_str(); let args: HashMap<_, &Expression> = ast_generator .properties @@ -54,6 +62,7 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno }) .collect::>>()?; + // E.g., "library" if let Some(expr) = args.get(ENGINE_TYPE_KEY) { if !expr.is_string() { diagnostics.push_error(DatamodelError::new_type_mismatch_error( @@ -65,6 +74,7 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno } } + // E.g., "prisma-client-js" let provider = match args.get(PROVIDER_KEY) { Some(val) => StringFromEnvVar::coerce(val, diagnostics)?, None => { @@ -92,7 +102,7 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno let preview_features = args .get(PREVIEW_FEATURES_KEY) .and_then(|v| coerce_array(v, &coerce::string, diagnostics).map(|arr| (arr, v.span()))) - .map(|(arr, span)| parse_and_validate_preview_features(arr, &ALL_PREVIEW_FEATURES, span, diagnostics)); + .map(|(arr, span)| parse_and_validate_preview_features(arr, feature_map_with_provider, span, diagnostics)); for prop in &ast_generator.properties { let is_first_class_prop = FIRST_CLASS_PROPERTIES.iter().any(|k| *k == prop.name()); @@ -130,7 +140,7 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno fn parse_and_validate_preview_features( preview_features: Vec<&str>, - feature_map: &FeatureMap, + feature_map_with_provider: &FeatureMapWithProvider, span: ast::Span, diagnostics: &mut Diagnostics, ) -> BitFlags { @@ -139,15 +149,44 @@ fn parse_and_validate_preview_features( for feature_str in preview_features { let feature_opt = PreviewFeature::parse_opt(feature_str); match feature_opt { - Some(feature) if feature_map.is_deprecated(feature) => { - features |= feature; - diagnostics.push_warning(DatamodelWarning::new_feature_deprecated(feature_str, span)); + Some(feature) if feature_map_with_provider.is_deprecated(feature) => { + match feature_map_with_provider.is_renamed(feature) { + Some(RenamedFeature::AllProviders(renamed_feature)) => { + features |= renamed_feature.to; + + diagnostics.push_warning(DatamodelWarning::new_preview_feature_renamed( + feature_str, + renamed_feature.to, + renamed_feature.prisly_link_endpoint, + span, + )); + } + Some(RenamedFeature::ForProvider((provider, renamed_feature))) => { + features |= renamed_feature.to; + + diagnostics.push_warning(DatamodelWarning::new_preview_feature_renamed_for_provider( + provider, + feature_str, + renamed_feature.to, + renamed_feature.prisly_link_endpoint, + span, + )); + } + None => { + features |= feature; + diagnostics.push_warning(DatamodelWarning::new_preview_feature_deprecated(feature_str, span)); + } + } } - Some(feature) if !feature_map.is_valid(feature) => { + Some(feature) if !feature_map_with_provider.is_valid(feature) => { diagnostics.push_error(DatamodelError::new_preview_feature_not_known_error( feature_str, - feature_map.active_features().iter().map(|pf| pf.to_string()).join(", "), + feature_map_with_provider + .active_features() + .iter() + .map(|pf| pf.to_string()) + .join(", "), span, )) } @@ -156,7 +195,11 @@ fn parse_and_validate_preview_features( None => diagnostics.push_error(DatamodelError::new_preview_feature_not_known_error( feature_str, - feature_map.active_features().iter().map(|pf| pf.to_string()).join(", "), + feature_map_with_provider + .active_features() + .iter() + .map(|pf| pf.to_string()) + .join(", "), span, )), } diff --git a/psl/psl/tests/config/nice_warnings.rs b/psl/psl/tests/config/nice_warnings.rs index 955cbbd89fd3..8a4a48271516 100644 --- a/psl/psl/tests/config/nice_warnings.rs +++ b/psl/psl/tests/config/nice_warnings.rs @@ -12,7 +12,7 @@ fn nice_warning_for_deprecated_generator_preview_feature() { let res = psl::parse_configuration(schema).unwrap(); - res.warnings.assert_is(DatamodelWarning::new_feature_deprecated( + res.warnings.assert_is(DatamodelWarning::new_preview_feature_deprecated( "middlewares", Span::new(88, 103, psl_core::parser_database::FileId::ZERO), )); diff --git a/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/mysql.prisma b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/mysql.prisma new file mode 100644 index 000000000000..9b735592b1ca --- /dev/null +++ b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/mysql.prisma @@ -0,0 +1,22 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["nativeFullTextSearchPostgres"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@fulltext([content, title]) +} +// error: The preview feature "nativeFullTextSearchPostgres" is not known. Expected one of: deno, driverAdapters, metrics, multiSchema, nativeDistinct, postgresqlExtensions, tracing, views, relationJoins, prismaSchemaFolder, omitApi, strictUndefinedChecks +// --> schema.prisma:3 +//  |  +//  2 |  provider = "prisma-client-js" +//  3 |  previewFeatures = ["nativeFullTextSearchPostgres"] +//  |  diff --git a/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgres.prisma b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgres.prisma new file mode 100644 index 000000000000..0dfbbe4e456a --- /dev/null +++ b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgres.prisma @@ -0,0 +1,16 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["nativeFullTextSearchPostgres"] +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@index([content, title]) +} \ No newline at end of file diff --git a/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgresql.prisma b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgresql.prisma new file mode 100644 index 000000000000..085f26d908c1 --- /dev/null +++ b/psl/psl/tests/validation/preview_features/native_full_text_search_postgres/postgresql.prisma @@ -0,0 +1,16 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["nativeFullTextSearchPostgres"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@index([content, title]) +} \ No newline at end of file diff --git a/psl/psl/tests/validation/preview_features/old_full_text_search/mysql.prisma b/psl/psl/tests/validation/preview_features/old_full_text_search/mysql.prisma new file mode 100644 index 000000000000..a752a92d256a --- /dev/null +++ b/psl/psl/tests/validation/preview_features/old_full_text_search/mysql.prisma @@ -0,0 +1,22 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch"] +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@fulltext([content, title]) +} +// warning: Preview feature "fullTextSearch" is deprecated. The functionality can be used without specifying it as a preview feature. +// --> schema.prisma:3 +//  |  +//  2 |  provider = "prisma-client-js" +//  3 |  previewFeatures = ["fullTextSearch"] +//  |  diff --git a/psl/psl/tests/validation/preview_features/old_full_text_search/postgres.prisma b/psl/psl/tests/validation/preview_features/old_full_text_search/postgres.prisma new file mode 100644 index 000000000000..273762067484 --- /dev/null +++ b/psl/psl/tests/validation/preview_features/old_full_text_search/postgres.prisma @@ -0,0 +1,22 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch"] +} + +datasource db { + provider = "postgres" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@index([content, title]) +} +// warning: On `provider = "postgresql"`, preview feature "fullTextSearch" has been renamed as "nativeFullTextSearchPostgres". Learn more at https://pris.ly/d/native-fts-postgres. +// --> schema.prisma:3 +//  |  +//  2 |  provider = "prisma-client-js" +//  3 |  previewFeatures = ["fullTextSearch"] +//  |  diff --git a/psl/psl/tests/validation/preview_features/old_full_text_search/postgresql.prisma b/psl/psl/tests/validation/preview_features/old_full_text_search/postgresql.prisma new file mode 100644 index 000000000000..e88fc8c71a0e --- /dev/null +++ b/psl/psl/tests/validation/preview_features/old_full_text_search/postgresql.prisma @@ -0,0 +1,22 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["fullTextSearch"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Blog { + id Int @unique + content String + title String + @@index([content, title]) +} +// warning: On `provider = "postgresql"`, preview feature "fullTextSearch" has been renamed as "nativeFullTextSearchPostgres". Learn more at https://pris.ly/d/native-fts-postgres. +// --> schema.prisma:3 +//  |  +//  2 |  provider = "prisma-client-js" +//  3 |  previewFeatures = ["fullTextSearch"] +//  |