diff --git a/Cargo.lock b/Cargo.lock index cdb132ce690c..9d7dff484d83 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1020,6 +1020,7 @@ dependencies = [ "base64 0.13.1", "expect-test", "indoc 2.0.3", + "itertools 0.12.0", "once_cell", "psl", "regex", @@ -2545,6 +2546,7 @@ dependencies = [ "expect-test", "futures", "indoc 2.0.3", + "itertools 0.12.0", "mongodb", "mongodb-client", "mongodb-schema-describer", @@ -5023,6 +5025,7 @@ dependencies = [ "enumflags2", "expect-test", "indoc 2.0.3", + "itertools 0.12.0", "pretty_assertions", "psl", "quaint", diff --git a/psl/parser-database/src/files.rs b/psl/parser-database/src/files.rs index b43154927c3c..c3ab72cbccfa 100644 --- a/psl/parser-database/src/files.rs +++ b/psl/parser-database/src/files.rs @@ -54,6 +54,12 @@ impl Files { String::from_utf8(out).unwrap() } + + /// Returns the number of files. + #[allow(clippy::len_without_is_empty)] + pub fn len(&self) -> usize { + self.0.len() + } } impl Index for Files { diff --git a/psl/parser-database/src/lib.rs b/psl/parser-database/src/lib.rs index 4f6c81257740..acbd1fedb56b 100644 --- a/psl/parser-database/src/lib.rs +++ b/psl/parser-database/src/lib.rs @@ -205,6 +205,11 @@ impl ParserDatabase { self.types.model_attributes.len() } + /// The total number of files for the schema. This is O(1). + pub fn files_count(&self) -> usize { + self.asts.len() + } + /// The source file contents. This methods asserts that there is a single prisma schema file. /// As multi-file schemas are implemented, calls to this methods should be replaced with /// `ParserDatabase::source()` and `ParserDatabase::iter_sources()`. diff --git a/psl/parser-database/src/walkers.rs b/psl/parser-database/src/walkers.rs index 0fc402a15341..040aa004c38c 100644 --- a/psl/parser-database/src/walkers.rs +++ b/psl/parser-database/src/walkers.rs @@ -79,6 +79,15 @@ impl crate::ParserDatabase { .map(|model_id| self.walk(model_id)) } + /// Find a composite type by name. + pub fn find_composite_type<'db>(&'db self, name: &str) -> Option> { + self.interner + .lookup(name) + .and_then(|name_id| self.names.tops.get(&name_id)) + .and_then(|(file_id, top_id)| top_id.as_composite_type_id().map(|id| (*file_id, id))) + .map(|ct_id| self.walk(ct_id)) + } + /// Traverse a schema element by id. pub fn walk(&self, id: I) -> Walker<'_, I> { Walker { db: self, id } diff --git a/psl/parser-database/src/walkers/composite_type.rs b/psl/parser-database/src/walkers/composite_type.rs index af286e9d0f2d..a4c3587e432b 100644 --- a/psl/parser-database/src/walkers/composite_type.rs +++ b/psl/parser-database/src/walkers/composite_type.rs @@ -28,6 +28,11 @@ impl<'db> CompositeTypeWalker<'db> { self.id } + /// The ID of the file containing the composite type. + pub fn file_id(self) -> FileId { + self.id.0 + } + /// The composite type node in the AST. pub fn ast_composite_type(self) -> &'db ast::CompositeType { &self.db.asts[self.id] diff --git a/psl/parser-database/src/walkers/model.rs b/psl/parser-database/src/walkers/model.rs index 262e25b0b187..87ac4085069d 100644 --- a/psl/parser-database/src/walkers/model.rs +++ b/psl/parser-database/src/walkers/model.rs @@ -25,6 +25,11 @@ impl<'db> ModelWalker<'db> { self.ast_model().name() } + /// The ID of the file containing the model. + pub fn file_id(self) -> FileId { + self.id.0 + } + /// Traverse the fields of the models in the order they were defined. pub fn fields(self) -> impl ExactSizeIterator> + Clone { self.ast_model() diff --git a/psl/psl-core/src/configuration/configuration_struct.rs b/psl/psl-core/src/configuration/configuration_struct.rs index 2914092822f1..0eca251b338c 100644 --- a/psl/psl-core/src/configuration/configuration_struct.rs +++ b/psl/psl-core/src/configuration/configuration_struct.rs @@ -14,10 +14,22 @@ pub struct Configuration { } impl Configuration { - pub fn extend(&mut self, configuration: Configuration) { - self.generators.extend(configuration.generators); - self.datasources.extend(configuration.datasources); - self.warnings.extend(configuration.warnings); + pub fn new( + generators: Vec, + datasources: Vec, + warnings: Vec, + ) -> Self { + Self { + generators, + datasources, + warnings, + } + } + + pub fn extend(&mut self, other: Configuration) { + self.generators.extend(other.generators); + self.datasources.extend(other.datasources); + self.warnings.extend(other.warnings); } pub fn validate_that_one_datasource_is_provided(&self) -> Result<(), Diagnostics> { @@ -167,4 +179,8 @@ impl Configuration { Ok(()) } + + pub fn first_datasource(&self) -> &Datasource { + self.datasources.first().expect("Expected a datasource to exist.") + } } diff --git a/psl/psl-core/src/configuration/datasource.rs b/psl/psl-core/src/configuration/datasource.rs index 8fdc4e5be885..f880f02050af 100644 --- a/psl/psl-core/src/configuration/datasource.rs +++ b/psl/psl-core/src/configuration/datasource.rs @@ -1,3 +1,5 @@ +use schema_ast::ast::WithSpan; + use crate::{ configuration::StringFromEnvVar, datamodel_connector::{Connector, ConnectorCapabilities, RelationMode}, @@ -274,6 +276,12 @@ impl Datasource { } } +impl WithSpan for Datasource { + fn span(&self) -> Span { + self.span + } +} + pub(crate) fn from_url(url: &StringFromEnvVar, env: F) -> Result where F: Fn(&str) -> Option, diff --git a/psl/psl-core/src/configuration/generator.rs b/psl/psl-core/src/configuration/generator.rs index 3c96a10cbc12..a17f1027641b 100644 --- a/psl/psl-core/src/configuration/generator.rs +++ b/psl/psl-core/src/configuration/generator.rs @@ -1,6 +1,8 @@ use crate::{configuration::StringFromEnvVar, PreviewFeature}; +use diagnostics::Span; use enumflags2::BitFlags; use parser_database::ast::Expression; +use schema_ast::ast::WithSpan; use serde::{ser::SerializeSeq, Serialize, Serializer}; use std::collections::HashMap; @@ -45,6 +47,15 @@ pub struct Generator { #[serde(skip_serializing_if = "Option::is_none")] pub documentation: Option, + + #[serde(skip)] + pub span: Span, +} + +impl WithSpan for Generator { + fn span(&self) -> Span { + self.span + } } pub fn mcf_preview_features(feats: &Option>, s: S) -> Result diff --git a/psl/psl-core/src/lib.rs b/psl/psl-core/src/lib.rs index ccf66ba32ffe..85fe3933924a 100644 --- a/psl/psl-core/src/lib.rs +++ b/psl/psl-core/src/lib.rs @@ -91,9 +91,7 @@ pub fn validate_multi_file(files: Vec<(String, SourceFile)>, connectors: Connect for ast in db.iter_asts() { let new_config = validate_configuration(ast, &mut diagnostics, connectors); - configuration.datasources.extend(new_config.datasources.into_iter()); - configuration.generators.extend(new_config.generators.into_iter()); - configuration.warnings.extend(new_config.warnings.into_iter()); + configuration.extend(new_config); } let datasources = &configuration.datasources; @@ -164,12 +162,7 @@ fn validate_configuration( 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); - Configuration { - generators, - datasources, - warnings: diagnostics.warnings().to_owned(), - } + Configuration::new(generators, datasources, diagnostics.warnings().to_owned()) } diff --git a/psl/psl-core/src/validate/datasource_loader.rs b/psl/psl-core/src/validate/datasource_loader.rs index 9f95c04230ae..fe43f4dd0d91 100644 --- a/psl/psl-core/src/validate/datasource_loader.rs +++ b/psl/psl-core/src/validate/datasource_loader.rs @@ -32,7 +32,7 @@ pub(crate) fn load_datasources_from_ast( for src in ast_schema.sources() { if let Some(source) = lift_datasource(src, diagnostics, connectors) { - sources.push(source) + sources.push(source); } } diff --git a/psl/psl-core/src/validate/generator_loader.rs b/psl/psl-core/src/validate/generator_loader.rs index 9f671b72ab91..7d3794d78232 100644 --- a/psl/psl-core/src/validate/generator_loader.rs +++ b/psl/psl-core/src/validate/generator_loader.rs @@ -26,7 +26,7 @@ pub(crate) fn load_generators_from_ast(ast_schema: &ast::SchemaAst, diagnostics: for gen in ast_schema.generators() { if let Some(generator) = lift_generator(gen, diagnostics) { - generators.push(generator) + generators.push(generator); } } @@ -123,6 +123,7 @@ fn lift_generator(ast_generator: &ast::GeneratorConfig, diagnostics: &mut Diagno preview_features, config: properties, documentation: ast_generator.documentation().map(String::from), + span: ast_generator.span, }) } diff --git a/psl/schema-ast/src/ast/find_at_position.rs b/psl/schema-ast/src/ast/find_at_position.rs index 2eddd34e86bc..f8fa23368cdd 100644 --- a/psl/schema-ast/src/ast/find_at_position.rs +++ b/psl/schema-ast/src/ast/find_at_position.rs @@ -352,7 +352,6 @@ impl<'ast> SourcePosition<'ast> { #[derive(Debug)] pub enum PropertyPosition<'ast> { - /// prop Property, Value(&'ast str), FunctionValue(&'ast str), diff --git a/psl/schema-ast/src/parser.rs b/psl/schema-ast/src/parser.rs index 185bb18c9173..392eb9e3d49a 100644 --- a/psl/schema-ast/src/parser.rs +++ b/psl/schema-ast/src/parser.rs @@ -18,4 +18,5 @@ pub use parse_schema::parse_schema; // It is more convenient if this enum is directly available here. #[derive(pest_derive::Parser)] #[grammar = "parser/datamodel.pest"] +#[allow(clippy::empty_docs)] pub(crate) struct PrismaDatamodelParser; diff --git a/schema-engine/connectors/mongodb-schema-connector/Cargo.toml b/schema-engine/connectors/mongodb-schema-connector/Cargo.toml index 111ece4d6ea3..af21f75c5b9a 100644 --- a/schema-engine/connectors/mongodb-schema-connector/Cargo.toml +++ b/schema-engine/connectors/mongodb-schema-connector/Cargo.toml @@ -31,3 +31,5 @@ once_cell = "1.8.0" url.workspace = true expect-test = "1" names = { version = "0.12", default-features = false } +itertools.workspace = true +indoc.workspace = true \ No newline at end of file diff --git a/schema-engine/connectors/mongodb-schema-connector/src/sampler.rs b/schema-engine/connectors/mongodb-schema-connector/src/sampler.rs index 779007115306..ec2d0c55c6fa 100644 --- a/schema-engine/connectors/mongodb-schema-connector/src/sampler.rs +++ b/schema-engine/connectors/mongodb-schema-connector/src/sampler.rs @@ -11,6 +11,7 @@ use mongodb::{ use mongodb_schema_describer::MongoSchema; use schema_connector::{warnings::Model, IntrospectionContext, IntrospectionResult, Warnings}; use statistics::*; +use std::borrow::Cow; /// From the given database, lists all collections as models, and samples /// maximum of SAMPLE_SIZE documents for their fields with the following rules: @@ -61,14 +62,25 @@ pub(super) async fn sample( } let mut data_model = render::Datamodel::default(); - statistics.render(ctx.datasource(), &mut data_model, &mut warnings); - let psl_string = if ctx.render_config { - let config = render::Configuration::from_psl(ctx.configuration(), None); - format!("{config}\n{data_model}") - } else { - data_model.to_string() - }; + // Ensures that all previous files are present in the new datamodel, even when empty after re-introspection. + for file_id in ctx.previous_schema().db.iter_file_ids() { + let file_name = ctx.previous_schema().db.file_name(file_id); + + data_model.create_empty_file(Cow::Borrowed(file_name)); + } + + statistics.render(ctx, &mut data_model, &mut warnings); + + let is_empty = data_model.is_empty(); + + if ctx.render_config { + let config = render::Configuration::from_psl(ctx.configuration(), ctx.previous_schema(), None); + + data_model.set_configuration(config); + } + + let sources = data_model.render(); let warnings = if !warnings.is_empty() { Some(warnings.to_string()) @@ -77,8 +89,8 @@ pub(super) async fn sample( }; Ok(IntrospectionResult { - data_model: psl::reformat(&psl_string, 2).unwrap(), - is_empty: data_model.is_empty(), + datamodels: psl::reformat_multiple(sources, 2), + is_empty, warnings, views: None, }) diff --git a/schema-engine/connectors/mongodb-schema-connector/src/sampler/statistics.rs b/schema-engine/connectors/mongodb-schema-connector/src/sampler/statistics.rs index 4ca4431caca9..1a46ad40dfea 100644 --- a/schema-engine/connectors/mongodb-schema-connector/src/sampler/statistics.rs +++ b/schema-engine/connectors/mongodb-schema-connector/src/sampler/statistics.rs @@ -8,7 +8,7 @@ use renderer::{ }; use schema_connector::{ warnings::{ModelAndField, ModelAndFieldAndType, TypeAndField, TypeAndFieldAndType}, - CompositeTypeDepth, Warnings, + CompositeTypeDepth, IntrospectionContext, Warnings, }; use super::field_type::FieldType; @@ -20,6 +20,7 @@ use once_cell::sync::Lazy; use psl::datamodel_connector::constraint_names::ConstraintNames; use regex::Regex; use std::{ + borrow::Cow, cmp::Ordering, collections::{BTreeMap, HashMap, HashSet}, fmt, @@ -121,7 +122,7 @@ impl<'a> Statistics<'a> { pub(super) fn render( &'a self, - datasource: &'a psl::Datasource, + ctx: &'a IntrospectionContext, rendered: &mut renderer::Datamodel<'a>, warnings: &mut Warnings, ) { @@ -164,7 +165,7 @@ impl<'a> Statistics<'a> { let mut field = renderer::datamodel::Field::new("id", "String"); field.map("_id"); - field.native_type(&datasource.name, "ObjectId", Vec::new()); + field.native_type(&ctx.datasource().name, "ObjectId", Vec::new()); field.default(renderer::datamodel::DefaultValue::function(Function::new("auto"))); field.id(IdFieldDefinition::new()); @@ -285,7 +286,7 @@ impl<'a> Statistics<'a> { } if let Some(native_type) = field_type.native_type() { - field.native_type(&datasource.name, native_type.to_string(), Vec::new()); + field.native_type(&ctx.datasource().name, native_type.to_string(), Vec::new()); } if field_type.is_array() { @@ -410,12 +411,22 @@ impl<'a> Statistics<'a> { } } - for (_, r#type) in types { - rendered.push_composite_type(r#type); + for (ct_name, r#type) in types { + let file_name = match ctx.previous_schema().db.find_composite_type(ct_name) { + Some(walker) => ctx.previous_schema().db.file_name(walker.file_id()), + None => ctx.introspection_file_name(), + }; + + rendered.push_composite_type(Cow::Borrowed(file_name), r#type); } - for (_, model) in models.into_iter() { - rendered.push_model(model); + for (model_name, model) in models.into_iter() { + let file_name = match ctx.previous_schema().db.find_model(model_name) { + Some(walker) => ctx.previous_schema().db.file_name(walker.file_id()), + None => ctx.introspection_file_name(), + }; + + rendered.push_model(Cow::Borrowed(file_name), model); } } diff --git a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/mod.rs b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/mod.rs index 8b61277da60a..4028c4c1abcd 100644 --- a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/mod.rs +++ b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/mod.rs @@ -4,6 +4,7 @@ mod basic; mod dirty_data; mod index; mod model_renames; +mod multi_file; mod remapping_names; mod types; mod views; diff --git a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/multi_file/mod.rs b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/multi_file/mod.rs new file mode 100644 index 000000000000..eb6d143f526c --- /dev/null +++ b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/multi_file/mod.rs @@ -0,0 +1,772 @@ +use crate::introspection::test_api::*; +use mongodb::bson::doc; + +// Composite types +// reintrospect_removed_model_single_file +// reintrospect_removed_model_multi_file + +// ----- Models ----- + +#[test] +fn reintrospect_new_model_single_file() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + seed_model("B", &api).await?; + + let input_dms = [("main.prisma", model_block_with_config("A", &api))]; + + let expected = expect![[r#" + // file: main.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_new_model_multi_file() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + seed_model("B", &api).await?; + seed_model("C", &api).await?; + + let input_dms = [ + ("a.prisma", model_block_with_config("A", &api)), + ("b.prisma", model_block("B")), + ]; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: b.prisma + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: introspected.prisma + model C { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_removed_model_single_file() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + seed_model("B", &api).await?; + + let input_dms = [( + "main.prisma", + [model_block_with_config("A", &api), model_block("B"), model_block("C")].join("\n"), + )]; + + let expected = expect![[r#" + // file: main.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_removed_model_multi_file() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + seed_model("B", &api).await?; + + let input_dms = [ + ("a.prisma", model_block_with_config("A", &api)), + ("b.prisma", model_block("B")), + ("c.prisma", model_block("C")), + ]; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: b.prisma + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: c.prisma + + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +// ----- Composite types ----- + +#[test] +fn reintrospect_new_composite_single_file() { + with_database(|mut api| async move { + seed_composite("A", &api).await?; + seed_composite("B", &api).await?; + + let input_dms = [("main.prisma", composite_block_with_config("A", &api))]; + + let expected = expect![[r#" + // file: main.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + type AIdentity { + firstName String + lastName String + } + + type BIdentity { + firstName String + lastName String + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity BIdentity + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_new_composite_multi_file() { + with_database(|mut api| async move { + seed_composite("A", &api).await?; + seed_composite("B", &api).await?; + seed_composite("C", &api).await?; + + let input_dms = [ + ("a.prisma", composite_block_with_config("A", &api)), + ("b.prisma", composite_block("B")), + ]; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + type AIdentity { + firstName String + lastName String + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + } + ------ + // file: b.prisma + type BIdentity { + firstName String + lastName String + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity BIdentity + } + ------ + // file: introspected.prisma + type CIdentity { + firstName String + lastName String + } + + model C { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity CIdentity + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_composite_model_single_file() { + with_database(|mut api| async move { + seed_composite("A", &api).await?; + seed_composite("B", &api).await?; + + let input_dms = [( + "main.prisma", + [ + composite_block_with_config("A", &api), + composite_block("B"), + composite_block("C"), + ] + .join("\n"), + )]; + + let expected = expect![[r#" + // file: main.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + type AIdentity { + firstName String + lastName String + } + + type BIdentity { + firstName String + lastName String + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity BIdentity + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_removed_composite_multi_file() { + with_database(|mut api| async move { + seed_composite("A", &api).await?; + seed_composite("B", &api).await?; + + let input_dms = [ + ("a.prisma", composite_block_with_config("A", &api)), + ("b.prisma", composite_block("B")), + ("c.prisma", composite_block("C")), + ]; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + type AIdentity { + firstName String + lastName String + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + } + ------ + // file: b.prisma + type BIdentity { + firstName String + lastName String + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity BIdentity + } + ------ + // file: c.prisma + + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_with_existing_composite_type() { + with_database(|mut api| async move { + seed_composite("A", &api).await?; + seed_composite("B", &api).await?; + + let a_dm = indoc::formatdoc! {r#" + {config} + + model A {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + identity Identity + }} + + type Identity {{ + firstName String + lastName String + }} + "#, + config = config_block_string(api.features)}; + + let b_dm = indoc::formatdoc! {r#" + model B {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + identity Identity + }} + + type Identity {{ + firstName String + lastName String + }} + "#}; + let input_dms = [("a.prisma", a_dm), ("b.prisma", b_dm)]; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + } + ------ + // file: b.prisma + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + identity BIdentity + } + ------ + // file: introspected.prisma + type AIdentity { + firstName String + lastName String + } + + type BIdentity { + firstName String + lastName String + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +// ----- Configuration ----- + +#[test] +fn reintrospect_keep_configuration_when_spread_across_files() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + seed_model("B", &api).await?; + + let expected = expect![[r#" + // file: a.prisma + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: b.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + "#]]; + + api.re_introspect_multi( + &[ + ("a.prisma", model_block_with_datasource("A")), + ("b.prisma", model_block_with_generator("B", &api)), + ], + expected, + ) + .await; + + let expected = expect![[r#" + // file: a.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: b.prisma + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model B { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + "#]]; + + api.re_introspect_multi( + &[ + ("a.prisma", model_block_with_generator("A", &api)), + ("b.prisma", model_block_with_datasource("B")), + ], + expected, + ) + .await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_keep_configuration_when_no_models() { + with_database(|mut api| async move { + seed_model("A", &api).await?; + + let input_dms = [ + ("a.prisma", model_block_with_datasource("A")), + ("b.prisma", model_block_with_generator("B", &api)), + ]; + + let expected = expect![[r#" + // file: a.prisma + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + + model A { + id String @id @default(auto()) @map("_id") @db.ObjectId + name String + } + ------ + // file: b.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +#[test] +fn reintrospect_empty_multi_file() { + with_database(|mut api| async move { + let input_dms = [ + ("a.prisma", model_block_with_datasource("A")), + ("b.prisma", model_block_with_generator("B", &api)), + ]; + + let expected = expect![[r#" + // file: a.prisma + datasource db { + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + } + ------ + // file: b.prisma + generator js { + provider = "prisma-client-js" + previewFeatures = [] + } + "#]]; + + api.re_introspect_multi(&input_dms, expected).await; + + let expected = expect![]; + + api.expect_warnings(&expected).await; + + Ok(()) + }) + .unwrap() +} + +async fn seed_model(name: &str, api: &TestApi) -> Result<(), mongodb::error::Error> { + let db = &api.db; + db.create_collection(name, None).await?; + let collection = db.collection(name); + collection.insert_many(vec![doc! {"name": "John"}], None).await.unwrap(); + + Ok(()) +} + +async fn seed_composite(name: &str, api: &TestApi) -> Result<(), mongodb::error::Error> { + let db = &api.db; + db.create_collection(name, None).await?; + let collection = db.collection(name); + collection + .insert_many( + vec![doc! {"identity": { "firstName": "John", "lastName": "Doe" }}], + None, + ) + .await + .unwrap(); + + Ok(()) +} + +fn model_block_with_datasource(name: &str) -> String { + indoc::formatdoc! {r#" + {config} + + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + }} + "#, + config = datasource_block_string()} +} + +fn model_block_with_generator(name: &str, api: &TestApi) -> String { + indoc::formatdoc! {r#" + {config} + + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + }} + "#, + config = generator_block_string(api.features)} +} + +fn model_block_with_config(name: &str, api: &TestApi) -> String { + indoc::formatdoc! {r#" + {config} + + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + }} + "#, + config = config_block_string(api.features)} +} + +fn model_block(name: &str) -> String { + indoc::formatdoc! {r#" + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + }} + "#} +} + +fn composite_block_with_config(name: &str, api: &TestApi) -> String { + indoc::formatdoc! {r#" + {config} + + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + }} + + type {name}Identity {{ + firstName String + lastName String + }} + "#, + config = config_block_string(api.features)} +} + +fn composite_block(name: &str) -> String { + indoc::formatdoc! {r#" + model {name} {{ + id String @id @default(auto()) @map("_id") @db.ObjectId + identity AIdentity + }} + + type {name}Identity {{ + firstName String + lastName String + }} + "#} +} diff --git a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/mod.rs b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/mod.rs index edead329db5a..f09c7071f1e8 100644 --- a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/mod.rs +++ b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/mod.rs @@ -1,29 +1,20 @@ +mod utils; + use enumflags2::BitFlags; +pub use expect_test::expect; use expect_test::Expect; +use itertools::Itertools; use mongodb::Database; use mongodb_schema_connector::MongoDbSchemaConnector; -use names::Generator; use once_cell::sync::Lazy; use psl::PreviewFeature; -use schema_connector::{CompositeTypeDepth, ConnectorParams, IntrospectionContext, SchemaConnector}; -use std::{future::Future, io::Write}; +use schema_connector::{ + CompositeTypeDepth, ConnectorParams, IntrospectionContext, IntrospectionResult, SchemaConnector, +}; +use std::future::Future; use tokio::runtime::Runtime; -pub use expect_test::expect; - -pub static CONN_STR: Lazy = Lazy::new(|| match std::env::var("TEST_DATABASE_URL") { - Ok(url) => url, - Err(_) => { - let stderr = std::io::stderr(); - - let mut sink = stderr.lock(); - sink.write_all(b"Please set TEST_DATABASE_URL env var pointing to a MongoDB instance.") - .unwrap(); - sink.write_all(b"\n").unwrap(); - - std::process::exit(1) - } -}); +pub use utils::*; pub static RT: Lazy = Lazy::new(|| Runtime::new().unwrap()); @@ -43,80 +34,147 @@ impl TestResult { } } -pub(super) fn introspect_features( - composite_type_depth: CompositeTypeDepth, +pub struct TestMultiResult { + datamodels: String, + warnings: String, +} + +impl TestMultiResult { + pub fn datamodels(&self) -> &str { + &self.datamodels + } +} + +impl From for TestResult { + fn from(res: IntrospectionResult) -> Self { + Self { + datamodel: res.datamodels.into_iter().next().unwrap().1, + warnings: res.warnings.unwrap_or_default(), + } + } +} + +impl From for TestMultiResult { + fn from(res: IntrospectionResult) -> Self { + let datamodels = res + .datamodels + .into_iter() + .sorted_unstable_by_key(|(file_name, _)| file_name.to_owned()) + .map(|(file_name, dm)| format!("// file: {file_name}\n{dm}")) + .join("------\n"); + + Self { + datamodels, + warnings: res.warnings.unwrap_or_default(), + } + } +} + +pub struct TestApi { + pub connection_string: String, + pub database_name: String, + pub db: Database, + pub features: BitFlags, + pub connector: MongoDbSchemaConnector, +} + +impl TestApi { + pub async fn re_introspect_multi(&mut self, datamodels: &[(&str, String)], expectation: expect_test::Expect) { + let schema = parse_datamodels(datamodels); + let ctx = IntrospectionContext::new(schema, CompositeTypeDepth::Infinite, None); + let reintrospected = self.connector.introspect(&ctx).await.unwrap(); + let reintrospected = TestMultiResult::from(reintrospected); + + expectation.assert_eq(reintrospected.datamodels()); + } + + pub async fn expect_warnings(&mut self, expectation: &expect_test::Expect) { + let previous_schema = psl::validate(config_block_string(self.features).into()); + let ctx = IntrospectionContext::new(previous_schema, CompositeTypeDepth::Infinite, None); + let result = self.connector.introspect(&ctx).await.unwrap(); + let result = TestMultiResult::from(result); + + expectation.assert_eq(&result.warnings); + } +} + +pub(super) fn with_database_features( + setup: F, preview_features: BitFlags, - init_database: F, -) -> TestResult +) -> Result where - F: FnOnce(Database) -> U, - U: Future>, + F: FnOnce(TestApi) -> U, + U: Future>, { - let mut names = Generator::default(); - - let database_name = names.next().unwrap().replace('-', ""); - let mut connection_string: url::Url = CONN_STR.parse().unwrap(); - connection_string.set_path(&format!( - "/{}{}", - database_name, - connection_string.path().trim_start_matches('/') - )); - let connection_string = connection_string.to_string(); - - let features = preview_features - .iter() - .map(|f| format!("\"{f}\"")) - .collect::>() - .join(", "); - - let datamodel_string = indoc::formatdoc!( - r#" - datasource db {{ - provider = "mongodb" - url = "{}" - }} - - generator js {{ - provider = "prisma-client-js" - previewFeatures = [{}] - }} - "#, - connection_string, - features, - ); - - let validated_schema = psl::parse_schema(datamodel_string).unwrap(); - let mut ctx = IntrospectionContext::new(validated_schema, composite_type_depth, None); - ctx.render_config = false; + let database_name = generate_database_name(); + let connection_string = get_connection_string(&database_name); RT.block_on(async move { let client = mongodb_client::create(&connection_string).await.unwrap(); let database = client.database(&database_name); let params = ConnectorParams { - connection_string, + connection_string: connection_string.clone(), preview_features, shadow_database_connection_string: None, }; - let mut connector = MongoDbSchemaConnector::new(params); + let connector = MongoDbSchemaConnector::new(params); - if init_database(database.clone()).await.is_err() { - database.drop(None).await.unwrap(); - } + let api = TestApi { + connection_string, + database_name, + db: database.clone(), + features: preview_features, + connector, + }; - let res = connector.introspect(&ctx).await; - database.drop(None).await.unwrap(); + let res = setup(api).await; - let res = res.unwrap(); + database.drop(None).await.unwrap(); - TestResult { - datamodel: res.data_model, - warnings: res.warnings.unwrap_or_default(), - } + res }) } +pub(super) fn with_database(setup: F) -> Result +where + F: FnMut(TestApi) -> U, + U: Future>, +{ + with_database_features(setup, BitFlags::empty()) +} + +pub(super) fn introspect_features( + composite_type_depth: CompositeTypeDepth, + preview_features: BitFlags, + init_database: F, +) -> TestResult +where + F: FnOnce(Database) -> U, + U: Future>, +{ + let datamodel_string = config_block_string(preview_features); + let validated_schema = psl::parse_schema(datamodel_string).unwrap(); + let ctx = IntrospectionContext::new(validated_schema, composite_type_depth, None).without_config_rendering(); + let res = with_database_features( + |mut api| async move { + init_database(api.db).await.unwrap(); + + let res = api.connector.introspect(&ctx).await.unwrap(); + + Ok(res) + }, + preview_features, + ) + .unwrap(); + + TestResult { + datamodel: res.datamodels.into_iter().next().unwrap().1, + warnings: res.warnings.unwrap_or_default(), + } +} + pub(super) fn introspect_depth(composite_type_depth: CompositeTypeDepth, init_database: F) -> TestResult where F: FnOnce(Database) -> U, diff --git a/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/utils.rs b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/utils.rs new file mode 100644 index 000000000000..38287300990d --- /dev/null +++ b/schema-engine/connectors/mongodb-schema-connector/tests/introspection/test_api/utils.rs @@ -0,0 +1,79 @@ +use enumflags2::BitFlags; +use names::Generator; +use once_cell::sync::Lazy; +use psl::PreviewFeature; +use std::io::Write as _; + +pub static CONN_STR: Lazy = Lazy::new(|| match std::env::var("TEST_DATABASE_URL") { + Ok(url) => url, + Err(_) => { + let stderr = std::io::stderr(); + + let mut sink = stderr.lock(); + sink.write_all(b"Please set TEST_DATABASE_URL env var pointing to a MongoDB instance.") + .unwrap(); + sink.write_all(b"\n").unwrap(); + + std::process::exit(1) + } +}); + +pub(crate) fn generate_database_name() -> String { + let mut names = Generator::default(); + + names.next().unwrap().replace('-', "") +} + +pub(crate) fn get_connection_string(database_name: &str) -> String { + let mut connection_string: url::Url = CONN_STR.parse().unwrap(); + connection_string.set_path(&format!( + "/{}{}", + database_name, + connection_string.path().trim_start_matches('/') + )); + + connection_string.to_string() +} + +pub(crate) fn datasource_block_string() -> String { + indoc::formatdoc!( + r#" + datasource db {{ + provider = "mongodb" + url = "env(TEST_DATABASE_URL)" + }} + "# + ) +} + +pub(crate) fn generator_block_string(features: BitFlags) -> String { + let features = features + .iter() + .map(|f| format!("\"{f}\"")) + .collect::>() + .join(", "); + + format!( + r#" + generator js {{ + provider = "prisma-client-js" + previewFeatures = [{}] + }} + "#, + features, + ) +} + +pub(crate) fn config_block_string(features: BitFlags) -> String { + format!("{}\n{}", generator_block_string(features), datasource_block_string()) +} + +#[track_caller] +pub(crate) fn parse_datamodels(datamodels: &[(&str, String)]) -> psl::ValidatedSchema { + let datamodels = datamodels + .iter() + .map(|(file_name, dm)| (file_name.to_string(), psl::SourceFile::from(dm))) + .collect(); + + psl::validate_multi_file(datamodels) +} diff --git a/schema-engine/connectors/schema-connector/src/introspection_context.rs b/schema-engine/connectors/schema-connector/src/introspection_context.rs index 62f116e5ca94..000ea92deae1 100644 --- a/schema-engine/connectors/schema-connector/src/introspection_context.rs +++ b/schema-engine/connectors/schema-connector/src/introspection_context.rs @@ -100,6 +100,23 @@ impl IntrospectionContext { name => unreachable!("The name `{}` for the datamodel connector is not known", name), } } + + /// Returns the file name into which new introspection data should be written. + pub fn introspection_file_name(&self) -> &str { + if self.previous_schema.db.files_count() == 1 { + let file_id = self.previous_schema.db.iter_file_ids().next().unwrap(); + + self.previous_schema.db.file_name(file_id) + } else { + "introspected.prisma" + } + } + + /// Removes the rendering of the configuration. + pub fn without_config_rendering(mut self) -> Self { + self.render_config = false; + self + } } /// Control type for composite type traversal. diff --git a/schema-engine/connectors/schema-connector/src/introspection_result.rs b/schema-engine/connectors/schema-connector/src/introspection_result.rs index 520dda74ee84..cc997617865f 100644 --- a/schema-engine/connectors/schema-connector/src/introspection_result.rs +++ b/schema-engine/connectors/schema-connector/src/introspection_result.rs @@ -15,7 +15,7 @@ pub struct ViewDefinition { #[derive(Debug)] pub struct IntrospectionResult { /// Datamodel - pub data_model: String, + pub datamodels: Vec<(String, String)>, /// The introspected data model is empty pub is_empty: bool, /// Introspection warnings @@ -25,13 +25,14 @@ pub struct IntrospectionResult { pub views: Option>, } -/// The output type from introspection. -#[derive(Debug, Deserialize, Serialize)] -pub struct IntrospectionResultOutput { - /// Datamodel - pub datamodel: String, - /// warnings - pub warnings: Option, - /// views - pub views: Option>, +impl IntrospectionResult { + /// Consumes the result and returns the first datamodel in the introspection result. + pub fn into_single_datamodel(mut self) -> String { + self.datamodels.remove(0).1 + } + + /// Returns the first datamodel in the introspection result. + pub fn single_datamodel(&self) -> &str { + &self.datamodels[0].1 + } } diff --git a/schema-engine/connectors/schema-connector/src/lib.rs b/schema-engine/connectors/schema-connector/src/lib.rs index b9c5a87f62d9..ad3e836df9e2 100644 --- a/schema-engine/connectors/schema-connector/src/lib.rs +++ b/schema-engine/connectors/schema-connector/src/lib.rs @@ -30,7 +30,7 @@ pub use destructive_change_checker::{ pub use diff::DiffTarget; pub use error::{ConnectorError, ConnectorResult}; pub use introspection_context::{CompositeTypeDepth, IntrospectionContext}; -pub use introspection_result::{IntrospectionResult, IntrospectionResultOutput, ViewDefinition}; +pub use introspection_result::{IntrospectionResult, ViewDefinition}; pub use migration::Migration; pub use migration_persistence::{MigrationPersistence, MigrationRecord, PersistenceNotInitializedError, Timestamp}; pub use warnings::Warnings; diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/datamodel_calculator.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/datamodel_calculator.rs index e30eb68f98c0..9da05c4bd6ae 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/datamodel_calculator.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/datamodel_calculator.rs @@ -9,14 +9,12 @@ use psl::PreviewFeature; use schema_connector::{IntrospectionContext, IntrospectionResult}; use sql_schema_describer as sql; -/// Calculate a data model from a database schema. +/// Calculate datamodels from a database schema. pub fn calculate(schema: &sql::SqlSchema, ctx: &IntrospectionContext, search_path: &str) -> IntrospectionResult { + let introspection_file_name = ctx.introspection_file_name(); let ctx = DatamodelCalculatorContext::new(ctx, schema, search_path); - let (schema_string, is_empty, views) = rendering::to_psl_string(&ctx); - let warnings = warnings::generate(&ctx); - - let empty_warnings = warnings.is_empty(); + let (datamodels, is_empty, views) = rendering::to_psl_string(introspection_file_name, &ctx); let views = if ctx.config.preview_features().contains(PreviewFeature::Views) { Some(views) @@ -24,14 +22,14 @@ pub fn calculate(schema: &sql::SqlSchema, ctx: &IntrospectionContext, search_pat None }; - let warnings = if empty_warnings { - None - } else { - Some(warnings.to_string()) + let warnings = warnings::generate(&ctx); + let warnings = match warnings.is_empty() { + true => None, + false => Some(warnings.to_string()), }; IntrospectionResult { - data_model: schema_string, + datamodels, is_empty, warnings, views, diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering.rs index a8b544e82561..c92281c95e5a 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering.rs @@ -15,26 +15,40 @@ use crate::introspection::datamodel_calculator::DatamodelCalculatorContext; use datamodel_renderer as renderer; use psl::PreviewFeature; use schema_connector::ViewDefinition; +use std::borrow::Cow; /// Combines the SQL database schema and an existing PSL schema to a /// PSL schema definition string. -pub(crate) fn to_psl_string(ctx: &DatamodelCalculatorContext<'_>) -> (String, bool, Vec) { - let mut rendered = renderer::Datamodel::new(); +pub(crate) fn to_psl_string( + introspection_file_name: &str, + ctx: &DatamodelCalculatorContext<'_>, +) -> (Vec<(String, String)>, bool, Vec) { + let mut datamodel = renderer::Datamodel::new(); let mut views = Vec::new(); - enums::render(ctx, &mut rendered); - models::render(ctx, &mut rendered); + // Ensures that all previous files are present in the new datamodel, even when empty after re-introspection. + for file_id in ctx.previous_schema.db.iter_file_ids() { + let file_name = ctx.previous_schema.db.file_name(file_id); + + datamodel.create_empty_file(Cow::Borrowed(file_name)); + } + + enums::render(introspection_file_name, ctx, &mut datamodel); + models::render(introspection_file_name, ctx, &mut datamodel); if ctx.config.preview_features().contains(PreviewFeature::Views) { - views.extend(views::render(ctx, &mut rendered)); + views.extend(views::render(introspection_file_name, ctx, &mut datamodel)); + } + + let is_empty = datamodel.is_empty(); + + if ctx.render_config { + let config = configuration::render(ctx.previous_schema, ctx.sql_schema, ctx.force_namespaces); + + datamodel.set_configuration(config); } - let psl_string = if ctx.render_config { - let config = configuration::render(ctx.config, ctx.sql_schema, ctx.force_namespaces); - format!("{config}\n{rendered}") - } else { - rendered.to_string() - }; + let sources = datamodel.render(); - (psl::reformat(&psl_string, 2).unwrap(), rendered.is_empty(), views) + (psl::reformat_multiple(sources, 2), is_empty, views) } diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/configuration.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/configuration.rs index 4f72894e3de1..1043f5a66ac0 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/configuration.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/configuration.rs @@ -1,27 +1,34 @@ //! Rendering of the datasource and generator parts of the PSL. use datamodel_renderer as render; -use psl::Configuration; +use psl::ValidatedSchema; use sql_schema_describer::SqlSchema; /// Render a configuration block. pub(super) fn render<'a>( - config: &'a Configuration, + previous_schema: &'a ValidatedSchema, schema: &'a SqlSchema, force_namespaces: Option<&'a [String]>, ) -> render::Configuration<'a> { + let prev_ds = previous_schema.configuration.first_datasource(); + let prev_ds_file_name = previous_schema.db.file_name(prev_ds.span.file_id); + let mut output = render::Configuration::default(); - let prev_ds = config.datasources.first().unwrap(); let mut datasource = render::configuration::Datasource::from_psl(prev_ds, force_namespaces); if prev_ds.active_connector.is_provider("postgres") { - super::postgres::add_extensions(&mut datasource, schema, config); + super::postgres::add_extensions(&mut datasource, schema, &previous_schema.configuration); } - output.push_datasource(datasource); + output.push_datasource(prev_ds_file_name.to_owned(), datasource); + + for prev_gen in &previous_schema.configuration.generators { + let prev_gen_file_name = previous_schema.db.file_name(prev_gen.span.file_id); - for prev in config.generators.iter() { - output.push_generator(render::configuration::Generator::from_psl(prev)); + output.push_generator( + prev_gen_file_name.to_owned(), + render::configuration::Generator::from_psl(prev_gen), + ); } output diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/enums.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/enums.rs index 11c87ab7de09..2385ea989c04 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/enums.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/enums.rs @@ -1,5 +1,7 @@ //! Rendering of enumerators. +use std::borrow::Cow; + use crate::introspection::{ datamodel_calculator::DatamodelCalculatorContext, introspection_helpers as helpers, introspection_pair::EnumPair, sanitize_datamodel_names, @@ -8,7 +10,11 @@ use datamodel_renderer::datamodel as renderer; use psl::parser_database as db; /// Render all enums. -pub(super) fn render<'a>(ctx: &'a DatamodelCalculatorContext<'a>, rendered: &mut renderer::Datamodel<'a>) { +pub(super) fn render<'a>( + introspection_file_name: &'a str, + ctx: &'a DatamodelCalculatorContext<'a>, + rendered: &mut renderer::Datamodel<'a>, +) { let mut all_enums: Vec<(Option, renderer::Enum<'_>)> = Vec::new(); for pair in ctx.enum_pairs() { @@ -25,8 +31,13 @@ pub(super) fn render<'a>(ctx: &'a DatamodelCalculatorContext<'a>, rendered: &mut }); } - for (_, enm) in all_enums { - rendered.push_enum(enm); + for (previous_schema_enum, enm) in all_enums { + let file_name = match previous_schema_enum { + Some((prev_file_id, _)) => ctx.previous_schema.db.file_name(prev_file_id), + None => introspection_file_name, + }; + + rendered.push_enum(Cow::Borrowed(file_name), enm); } } diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/models.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/models.rs index 91e07099e6dd..7d673ada07cd 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/models.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/models.rs @@ -1,5 +1,7 @@ //! Rendering of model blocks. +use std::borrow::Cow; + use super::{id, indexes, relation_field, scalar_field}; use crate::introspection::{ datamodel_calculator::DatamodelCalculatorContext, @@ -10,7 +12,11 @@ use datamodel_renderer::datamodel as renderer; use quaint::prelude::SqlFamily; /// Render all model blocks to the PSL. -pub(super) fn render<'a>(ctx: &'a DatamodelCalculatorContext<'a>, rendered: &mut renderer::Datamodel<'a>) { +pub(super) fn render<'a>( + introspection_file_name: &'a str, + ctx: &'a DatamodelCalculatorContext<'a>, + rendered: &mut renderer::Datamodel<'a>, +) { let mut models_with_idx: Vec<(Option<_>, renderer::Model<'a>)> = Vec::with_capacity(ctx.sql_schema.tables_count()); for model in ctx.model_pairs() { @@ -19,8 +25,13 @@ pub(super) fn render<'a>(ctx: &'a DatamodelCalculatorContext<'a>, rendered: &mut models_with_idx.sort_by(|(a, _), (b, _)| helpers::compare_options_none_last(*a, *b)); - for (_, render) in models_with_idx.into_iter() { - rendered.push_model(render); + for (previous_model, render) in models_with_idx.into_iter() { + let file_name = match previous_model { + Some((prev_file_id, _)) => ctx.previous_schema.db.file_name(prev_file_id), + None => introspection_file_name, + }; + + rendered.push_model(Cow::Borrowed(file_name), render); } } diff --git a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/views.rs b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/views.rs index baa507308f94..f4a7d0d09b4f 100644 --- a/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/views.rs +++ b/schema-engine/connectors/sql-schema-connector/src/introspection/rendering/views.rs @@ -4,9 +4,11 @@ use crate::introspection::{ }; use datamodel_renderer::datamodel as renderer; use schema_connector::ViewDefinition; +use std::borrow::Cow; /// Render all view blocks to the PSL. pub(super) fn render<'a>( + introspection_file_name: &'a str, ctx: &'a DatamodelCalculatorContext<'a>, rendered: &mut renderer::Datamodel<'a>, ) -> Vec { @@ -32,8 +34,13 @@ pub(super) fn render<'a>( views_with_idx.sort_by(|(a, _), (b, _)| helpers::compare_options_none_last(*a, *b)); - for (_, render) in views_with_idx.into_iter() { - rendered.push_view(render); + for (previous_view, render) in views_with_idx.into_iter() { + let file_name = match previous_view { + Some((previous_file_id, _)) => ctx.previous_schema.db.file_name(previous_file_id), + None => introspection_file_name, + }; + + rendered.push_view(Cow::Borrowed(file_name), render); } definitions diff --git a/schema-engine/core/src/state.rs b/schema-engine/core/src/state.rs index 5fb3d9db1976..3951c6b91a25 100644 --- a/schema-engine/core/src/state.rs +++ b/schema-engine/core/src/state.rs @@ -6,7 +6,7 @@ use crate::{api::GenericApi, commands, json_rpc::types::*, CoreError, CoreResult}; use enumflags2::BitFlags; use psl::{parser_database::SourceFile, PreviewFeature}; -use schema_connector::{ConnectorError, ConnectorHost, Namespaces, SchemaConnector}; +use schema_connector::{ConnectorError, ConnectorHost, IntrospectionResult, Namespaces, SchemaConnector}; use std::{collections::HashMap, future::Future, path::Path, pin::Pin, sync::Arc}; use tokio::sync::{mpsc, Mutex}; use tracing_futures::Instrument; @@ -355,12 +355,17 @@ impl GenericApi for EngineState { None, Box::new(move |connector| { Box::pin(async move { - let result = connector.introspect(&ctx).await?; - - if result.is_empty { + let IntrospectionResult { + mut datamodels, + views, + warnings, + is_empty, + } = connector.introspect(&ctx).await?; + + if is_empty { Err(ConnectorError::into_introspection_result_empty_error()) } else { - let views = result.views.map(|v| { + let views = views.map(|v| { v.into_iter() .map(|view| IntrospectionView { schema: view.schema, @@ -371,9 +376,9 @@ impl GenericApi for EngineState { }); Ok(IntrospectResult { - datamodel: result.data_model, + datamodel: datamodels.remove(0).1, views, - warnings: result.warnings, + warnings, }) } }) diff --git a/schema-engine/datamodel-renderer/Cargo.toml b/schema-engine/datamodel-renderer/Cargo.toml index 4b2b07a3d03a..ad1b0435d66b 100644 --- a/schema-engine/datamodel-renderer/Cargo.toml +++ b/schema-engine/datamodel-renderer/Cargo.toml @@ -12,3 +12,4 @@ base64 = "0.13.1" [dev-dependencies] expect-test = "1.4.0" indoc.workspace = true +itertools.workspace = true diff --git a/schema-engine/datamodel-renderer/src/configuration.rs b/schema-engine/datamodel-renderer/src/configuration.rs index 4f5d043b1698..43a1a1703293 100644 --- a/schema-engine/datamodel-renderer/src/configuration.rs +++ b/schema-engine/datamodel-renderer/src/configuration.rs @@ -7,38 +7,54 @@ mod generator; pub use datasource::Datasource; pub use generator::Generator; +use psl::ValidatedSchema; +use std::borrow::Cow; +use std::collections::HashMap; use std::fmt; /// The configuration part of a data model. First the generators, then /// the datasources. #[derive(Debug, Default)] pub struct Configuration<'a> { - generators: Vec>, - datasources: Vec>, + /// Generators blocks by file name. + pub generators: HashMap, Vec>>, + /// Datasources blocks by file name. + pub datasources: HashMap, Vec>>, } impl<'a> Configuration<'a> { /// Add a new generator to the configuration. - pub fn push_generator(&mut self, generator: Generator<'a>) { - self.generators.push(generator); + pub fn push_generator(&mut self, file: impl Into>, generator: Generator<'a>) { + self.generators.entry(file.into()).or_default().push(generator); } /// Add a new datasource to the configuration. - pub fn push_datasource(&mut self, datasource: Datasource<'a>) { - self.datasources.push(datasource); + pub fn push_datasource(&mut self, file: impl Into>, datasource: Datasource<'a>) { + self.datasources.entry(file.into()).or_default().push(datasource); } /// Create a rendering from a PSL datasource. - pub fn from_psl(psl_cfg: &'a psl::Configuration, force_namespaces: Option<&'a [String]>) -> Self { + pub fn from_psl( + psl_cfg: &'a psl::Configuration, + prev_schema: &'a ValidatedSchema, + force_namespaces: Option<&'a [String]>, + ) -> Self { let mut config = Self::default(); - for generator in psl_cfg.generators.iter() { - config.push_generator(Generator::from_psl(generator)); + for generator in &psl_cfg.generators { + let file_name = prev_schema.db.file_name(generator.span.file_id); + + config.push_generator(Cow::Borrowed(file_name), Generator::from_psl(generator)); } - for datasource in psl_cfg.datasources.iter() { - config.push_datasource(Datasource::from_psl(datasource, force_namespaces)); + for datasource in &psl_cfg.datasources { + let file_name = prev_schema.db.file_name(datasource.span.file_id); + + config.push_datasource( + Cow::Borrowed(file_name), + Datasource::from_psl(datasource, force_namespaces), + ); } config @@ -47,12 +63,16 @@ impl<'a> Configuration<'a> { impl<'a> fmt::Display for Configuration<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for generator in self.generators.iter() { - generator.fmt(f)? + for (_, generators) in self.generators.iter() { + for generator in generators { + generator.fmt(f)? + } } - for datasource in self.datasources.iter() { - datasource.fmt(f)?; + for (_, datasources) in self.datasources.iter() { + for datasource in datasources { + datasource.fmt(f)?; + } } Ok(()) @@ -67,9 +87,16 @@ mod tests { #[test] fn minimal() { let mut config = Configuration::default(); + let file_name = "schema.prisma"; - config.push_generator(Generator::new("client", Env::value("prisma-client-js"))); - config.push_datasource(Datasource::new("db", "postgres", Env::variable("DATABASE_URL"))); + config.push_generator( + file_name.to_owned(), + Generator::new("client", Env::value("prisma-client-js")), + ); + config.push_datasource( + file_name.to_owned(), + Datasource::new("db", "postgres", Env::variable("DATABASE_URL")), + ); let rendered = psl::reformat(&format!("{config}"), 2).unwrap(); @@ -90,11 +117,24 @@ mod tests { #[test] fn not_so_minimal() { let mut config = Configuration::default(); - - config.push_generator(Generator::new("js", Env::value("prisma-client-js"))); - config.push_generator(Generator::new("go", Env::value("prisma-client-go"))); - config.push_datasource(Datasource::new("pg", "postgres", Env::variable("PG_DATABASE_URL"))); - config.push_datasource(Datasource::new("my", "mysql", Env::variable("MY_DATABASE_URL"))); + let file_name = "schema.prisma"; + + config.push_generator( + file_name.to_owned(), + Generator::new("js", Env::value("prisma-client-js")), + ); + config.push_generator( + file_name.to_owned(), + Generator::new("go", Env::value("prisma-client-go")), + ); + config.push_datasource( + file_name.to_owned(), + Datasource::new("pg", "postgres", Env::variable("PG_DATABASE_URL")), + ); + config.push_datasource( + file_name.to_owned(), + Datasource::new("my", "mysql", Env::variable("MY_DATABASE_URL")), + ); let expected = expect![[r#" generator js { diff --git a/schema-engine/datamodel-renderer/src/datamodel.rs b/schema-engine/datamodel-renderer/src/datamodel.rs index 6235bae3878c..a726dc6cad5a 100644 --- a/schema-engine/datamodel-renderer/src/datamodel.rs +++ b/schema-engine/datamodel-renderer/src/datamodel.rs @@ -19,17 +19,24 @@ pub use field::Field; pub use field_type::FieldType; pub use index::{IdDefinition, IdFieldDefinition, IndexDefinition, IndexFieldInput, IndexOps, UniqueFieldAttribute}; pub use model::{Model, Relation}; +use psl::SourceFile; pub use view::View; -use std::fmt; +use crate::Configuration; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, +}; /// The PSL data model declaration. #[derive(Default, Debug)] pub struct Datamodel<'a> { - models: Vec>, - views: Vec>, - enums: Vec>, - composite_types: Vec>, + models: HashMap, Vec>>, + views: HashMap, Vec>>, + enums: HashMap, Vec>>, + composite_types: HashMap, Vec>>, + configuration: Option>, + empty_files: HashSet>, } impl<'a> Datamodel<'a> { @@ -38,6 +45,11 @@ impl<'a> Datamodel<'a> { Self::default() } + /// Create an empty file in the data model. + pub fn create_empty_file(&mut self, file: impl Into>) { + self.empty_files.insert(file.into()); + } + /// Add a model block to the data model. /// /// ```ignore @@ -45,8 +57,8 @@ impl<'a> Datamodel<'a> { /// id Int @id // < this /// } // < /// ``` - pub fn push_model(&mut self, model: Model<'a>) { - self.models.push(model); + pub fn push_model(&mut self, file: impl Into>, model: Model<'a>) { + self.models.entry(file.into()).or_default().push(model); } /// Add an enum block to the data model. @@ -56,8 +68,8 @@ impl<'a> Datamodel<'a> { /// Bar // < this /// } // < /// ``` - pub fn push_enum(&mut self, r#enum: Enum<'a>) { - self.enums.push(r#enum); + pub fn push_enum(&mut self, file: impl Into>, r#enum: Enum<'a>) { + self.enums.entry(file.into()).or_default().push(r#enum); } /// Add a view block to the data model. @@ -67,8 +79,8 @@ impl<'a> Datamodel<'a> { /// id Int @id // < this /// } // < /// ``` - pub fn push_view(&mut self, view: View<'a>) { - self.views.push(view); + pub fn push_view(&mut self, file: impl Into>, view: View<'a>) { + self.views.entry(file.into()).or_default().push(view); } /// Add a composite type block to the data model. @@ -78,35 +90,85 @@ impl<'a> Datamodel<'a> { /// street String // < this /// } // < /// ``` - pub fn push_composite_type(&mut self, composite_type: CompositeType<'a>) { - self.composite_types.push(composite_type); + pub fn push_composite_type(&mut self, file: impl Into>, composite_type: CompositeType<'a>) { + self.composite_types + .entry(file.into()) + .or_default() + .push(composite_type); } /// True if the render output would be an empty string. pub fn is_empty(&self) -> bool { self.models.is_empty() && self.enums.is_empty() && self.composite_types.is_empty() && self.views.is_empty() } -} -impl<'a> fmt::Display for Datamodel<'a> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for ct in self.composite_types.iter() { - writeln!(f, "{ct}")?; + /// Renders the datamodel into a list of file names and their content. + pub fn render(self) -> Vec<(String, SourceFile)> { + let mut rendered: HashMap, String> = HashMap::new(); + + if let Some(config) = self.configuration { + for (file, generators) in config.generators { + let generator_str = rendered.entry(file).or_default(); + + for generator in generators { + generator_str.push_str(&format!("{generator}\n")); + } + } + + for (file, datasources) in config.datasources { + let datasource_str = rendered.entry(file).or_default(); + + for datasource in datasources { + datasource_str.push_str(&format!("{datasource}\n")); + } + } + } + + for (file, composite_types) in self.composite_types { + let composite_type_str = rendered.entry(file).or_default(); + + for composite_type in composite_types { + composite_type_str.push_str(&format!("{composite_type}\n")); + } } - for model in self.models.iter() { - writeln!(f, "{model}")?; + for (file, models) in self.models { + let model_str = rendered.entry(file).or_default(); + + for model in models { + model_str.push_str(&format!("{model}\n")); + } } - for view in self.views.iter() { - writeln!(f, "{view}")?; + for (file, views) in self.views { + let view_str = rendered.entry(file).or_default(); + + for view in views { + view_str.push_str(&format!("{view}\n")); + } } - for r#enum in self.enums.iter() { - writeln!(f, "{enum}")?; + for (file, enums) in self.enums { + let enum_str = rendered.entry(file).or_default(); + + for r#enum in enums { + enum_str.push_str(&format!("{enum}\n")); + } } - Ok(()) + for empty_file in self.empty_files { + rendered.entry(empty_file).or_default(); + } + + rendered + .into_iter() + .map(|(file, content)| (file.into_owned(), SourceFile::from(content))) + .collect() + } + + /// Sets the configuration blocks for a datamodel. + pub fn set_configuration(&mut self, config: Configuration<'a>) { + self.configuration = Some(config); } } @@ -116,16 +178,29 @@ mod tests { use super::*; use expect_test::expect; + use itertools::Itertools as _; + + fn pretty_render(data_model: Datamodel) -> String { + let sources = data_model.render(); + let sources = psl::reformat_multiple(sources, 2); + + sources + .into_iter() + .sorted_unstable_by_key(|(file_name, _)| file_name.to_owned()) + .map(|(file_name, dm)| format!("// file: {file_name}\n{}", dm.as_str())) + .join("------\n") + } #[test] fn simple_data_model() { + let file_name = "schema.prisma"; let mut data_model = Datamodel::new(); let mut composite = CompositeType::new("Address"); let field = Field::new("street", "String"); composite.push_field(field); - data_model.push_composite_type(composite); + data_model.push_composite_type(file_name.to_string(), composite); let mut model = Model::new("User"); @@ -136,7 +211,7 @@ mod tests { field.default(dv); model.push_field(field); - data_model.push_model(model); + data_model.push_model(file_name.to_string(), model); let mut traffic_light = Enum::new("TrafficLight"); @@ -144,13 +219,13 @@ mod tests { traffic_light.push_variant("Yellow"); traffic_light.push_variant("Green"); - data_model.push_enum(traffic_light); + data_model.push_enum(file_name.to_string(), traffic_light); let mut cat = Enum::new("Cat"); cat.push_variant("Asleep"); cat.push_variant("Hungry"); - data_model.push_enum(cat); + data_model.push_enum(file_name.to_string(), cat); let mut view = View::new("Meow"); let mut field = Field::new("id", "Int"); @@ -158,9 +233,10 @@ mod tests { view.push_field(field); - data_model.push_view(view); + data_model.push_view(file_name.to_string(), view); let expected = expect![[r#" + // file: schema.prisma type Address { street String } @@ -184,8 +260,84 @@ mod tests { Hungry } "#]]; + let rendered = pretty_render(data_model); + + expected.assert_eq(&rendered); + } + + #[test] + fn data_model_multi_file() { + let mut data_model = Datamodel::new(); + + let mut composite = CompositeType::new("Address"); + let field = Field::new("street", "String"); + composite.push_field(field); + + data_model.push_composite_type("a.prisma".to_string(), composite); + + let mut model = Model::new("User"); + + let mut field = Field::new("id", "Int"); + field.id(IdFieldDefinition::default()); + + let dv = DefaultValue::function(Function::new("autoincrement")); + field.default(dv); + + model.push_field(field); + data_model.push_model("a.prisma".to_string(), model); + + let mut traffic_light = Enum::new("TrafficLight"); + + traffic_light.push_variant("Red"); + traffic_light.push_variant("Yellow"); + traffic_light.push_variant("Green"); + + data_model.push_enum("b.prisma".to_string(), traffic_light); + + let mut cat = Enum::new("Cat"); + cat.push_variant("Asleep"); + cat.push_variant("Hungry"); + + data_model.push_enum("c.prisma".to_string(), cat); + + let mut view = View::new("Meow"); + let mut field = Field::new("id", "Int"); + field.id(IdFieldDefinition::default()); + + view.push_field(field); + + data_model.push_view("d.prisma".to_string(), view); + + let expected = expect![[r#" + // file: a.prisma + type Address { + street String + } + + model User { + id Int @id @default(autoincrement()) + } + ------ + // file: b.prisma + enum TrafficLight { + Red + Yellow + Green + } + ------ + // file: c.prisma + enum Cat { + Asleep + Hungry + } + ------ + // file: d.prisma + view Meow { + id Int @id + } + "#]]; + let rendered = pretty_render(data_model); - let rendered = psl::reformat(&format!("{data_model}"), 2).unwrap(); expected.assert_eq(&rendered); } } diff --git a/schema-engine/sql-introspection-tests/Cargo.toml b/schema-engine/sql-introspection-tests/Cargo.toml index 2822dfb3fd38..3d45c178f09f 100644 --- a/schema-engine/sql-introspection-tests/Cargo.toml +++ b/schema-engine/sql-introspection-tests/Cargo.toml @@ -13,6 +13,7 @@ user-facing-errors = { path = "../../libs/user-facing-errors", features = [ "all-native", ] } test-setup = { path = "../../libs/test-setup" } +itertools.workspace = true enumflags2.workspace = true connection-string.workspace = true diff --git a/schema-engine/sql-introspection-tests/src/test_api.rs b/schema-engine/sql-introspection-tests/src/test_api.rs index 524b55e9f2d0..d6eb9a349ec6 100644 --- a/schema-engine/sql-introspection-tests/src/test_api.rs +++ b/schema-engine/sql-introspection-tests/src/test_api.rs @@ -1,6 +1,7 @@ pub use super::TestResult; pub use expect_test::expect; pub use indoc::{formatdoc, indoc}; +use itertools::Itertools; pub use quaint::prelude::Queryable; use schema_connector::CompositeTypeDepth; use schema_connector::ConnectorResult; @@ -156,23 +157,52 @@ impl TestApi { pub async fn introspect(&mut self) -> Result { let previous_schema = psl::validate(self.pure_config().into()); - let introspection_result = self.test_introspect_internal(previous_schema, true).await?; + let introspection_result = self + .test_introspect_internal(previous_schema, true) + .await? + .to_single_test_result(); - Ok(introspection_result.data_model) + Ok(introspection_result.datamodel) + } + + pub async fn introspect_multi(&mut self) -> Result { + let previous_schema = psl::validate(self.pure_config().into()); + let introspection_result = self + .test_introspect_internal(previous_schema, true) + .await? + .to_multi_test_result(); + + Ok(introspection_result.datamodels) } pub async fn introspect_views(&mut self) -> Result>> { let previous_schema = psl::validate(self.pure_config().into()); - let introspection_result = self.test_introspect_internal(previous_schema, true).await?; + let introspection_result = self + .test_introspect_internal(previous_schema, true) + .await? + .to_single_test_result(); + + Ok(introspection_result.views) + } + + pub async fn introspect_views_multi(&mut self) -> Result>> { + let previous_schema = psl::validate(self.pure_config().into()); + let introspection_result = self + .test_introspect_internal(previous_schema, true) + .await? + .to_multi_test_result(); Ok(introspection_result.views) } pub async fn introspect_dml(&mut self) -> Result { let previous_schema = psl::validate(self.pure_config().into()); - let introspection_result = self.test_introspect_internal(previous_schema, false).await?; + let introspection_result = self + .test_introspect_internal(previous_schema, false) + .await? + .to_single_test_result(); - Ok(introspection_result.data_model) + Ok(introspection_result.datamodel) } pub fn is_cockroach(&self) -> bool { @@ -214,25 +244,34 @@ impl TestApi { pub async fn re_introspect(&mut self, data_model_string: &str) -> Result { let schema = format!("{}{}", self.pure_config(), data_model_string); let schema = parse_datamodel(&schema); - let introspection_result = self.test_introspect_internal(schema, true).await?; + let introspection_result = self + .test_introspect_internal(schema, true) + .await? + .to_single_test_result(); - Ok(introspection_result.data_model) + Ok(introspection_result.datamodel) } #[tracing::instrument(skip(self, data_model_string))] pub async fn re_introspect_dml(&mut self, data_model_string: &str) -> Result { let data_model = parse_datamodel(&format!("{}{}", self.pure_config(), data_model_string)); - let introspection_result = self.test_introspect_internal(data_model, false).await?; + let introspection_result = self + .test_introspect_internal(data_model, false) + .await? + .to_single_test_result(); - Ok(introspection_result.data_model) + Ok(introspection_result.datamodel) } #[tracing::instrument(skip(self, data_model_string))] pub async fn re_introspect_config(&mut self, data_model_string: &str) -> Result { let data_model = parse_datamodel(data_model_string); - let introspection_result = self.test_introspect_internal(data_model, true).await?; + let introspection_result = self + .test_introspect_internal(data_model, true) + .await? + .to_single_test_result(); - Ok(introspection_result.data_model) + Ok(introspection_result.datamodel) } pub async fn re_introspect_warnings(&mut self, data_model_string: &str) -> Result { @@ -325,8 +364,12 @@ impl TestApi { ) } - fn pure_config(&self) -> String { - format!("{}\n{}", &self.datasource_block_string(), &self.generator_block()) + pub fn pure_config(&self) -> String { + format!( + "{}\n{}", + &self.datasource_block_string(), + &self.generator_block_string() + ) } pub fn configuration(&self) -> Configuration { @@ -338,13 +381,29 @@ impl TestApi { expectation.assert_eq(&found); } + pub async fn expect_datamodels(&mut self, expectation: &expect_test::Expect) { + let found = self.introspect_multi().await.unwrap(); + + expectation.assert_eq(&found); + } + + fn process_views(&self, view_name: &str, views: Vec) -> ViewDefinition { + views + .into_iter() + .find(|v| v.schema == self.schema_name() && v.name == view_name) + .expect("Could not find view with the given name.") + } + pub async fn expect_view_definition(&mut self, view: &str, expectation: &expect_test::Expect) { let views = self.introspect_views().await.unwrap().unwrap_or_default(); + let view = self.process_views(view, views); - let view = views - .into_iter() - .find(|v| v.schema == self.schema_name() && v.name == view) - .expect("Could not find view with the given name."); + expectation.assert_eq(&view.definition); + } + + pub async fn expect_view_definition_multi(&mut self, view: &str, expectation: &expect_test::Expect) { + let views = self.introspect_views_multi().await.unwrap().unwrap_or_default(); + let view = self.process_views(view, views); expectation.assert_eq(&view.definition); } @@ -378,15 +437,48 @@ impl TestApi { let previous_schema = psl::validate(self.pure_config().into()); let introspection_result = self.test_introspect_internal(previous_schema, true).await.unwrap(); - dbg!(&introspection_result.warnings); assert!(introspection_result.warnings.is_none()) } pub async fn expect_re_introspected_datamodel(&mut self, schema: &str, expectation: expect_test::Expect) { let data_model = parse_datamodel(&format!("{}{}", self.pure_config(), schema)); - let reintrospected = self.test_introspect_internal(data_model, false).await.unwrap(); + let reintrospected = self + .test_introspect_internal(data_model, false) + .await + .unwrap() + .to_single_test_result(); + + expectation.assert_eq(&reintrospected.datamodel); + } + + pub async fn expect_re_introspected_datamodels( + &mut self, + datamodels: &[(&str, String)], + expectation: expect_test::Expect, + ) { + let schema = parse_datamodels(datamodels); + let reintrospected = self + .test_introspect_internal(schema, false) + .await + .unwrap() + .to_multi_test_result(); + + expectation.assert_eq(&reintrospected.datamodels); + } + + pub async fn expect_re_introspected_datamodels_with_config( + &mut self, + datamodels: &[(&str, String)], + expectation: expect_test::Expect, + ) { + let schema = parse_datamodels(datamodels); + let reintrospected = self + .test_introspect_internal(schema, true) + .await + .unwrap() + .to_multi_test_result(); - expectation.assert_eq(&reintrospected.data_model); + expectation.assert_eq(&reintrospected.datamodels); } pub async fn expect_re_introspect_warnings(&mut self, schema: &str, expectation: expect_test::Expect) { @@ -398,6 +490,18 @@ impl TestApi { expectation.assert_eq(&warnings); } + pub async fn expect_re_introspect_datamodels_warnings( + &mut self, + datamodels: &[(&str, String)], + expectation: expect_test::Expect, + ) { + let data_model = parse_datamodels(datamodels); + let introspection_result = self.test_introspect_internal(data_model, false).await.unwrap(); + let warnings = introspection_result.warnings.unwrap_or_default(); + + expectation.assert_eq(&warnings); + } + pub fn assert_eq_datamodels(&self, expected_without_header: &str, result_with_header: &str) { let expected_with_source = self.dm_with_sources(expected_without_header); let expected_with_generator = self.dm_with_generator_and_preview_flags(&expected_with_source); @@ -417,12 +521,12 @@ impl TestApi { fn dm_with_generator_and_preview_flags(&self, schema: &str) -> String { let mut out = String::with_capacity(320 + schema.len()); - write!(out, "{}\n{}", self.generator_block(), schema).unwrap(); + write!(out, "{}\n{}", self.generator_block_string(), schema).unwrap(); out } - fn generator_block(&self) -> String { + pub fn generator_block_string(&self) -> String { let preview_features: Vec = self.preview_features().iter().map(|pf| format!(r#""{pf}""#)).collect(); let preview_feature_string = if preview_features.is_empty() { @@ -448,3 +552,81 @@ impl TestApi { fn parse_datamodel(dm: &str) -> psl::ValidatedSchema { psl::parse_schema(dm).unwrap() } + +#[track_caller] +fn parse_datamodels(datamodels: &[(&str, String)]) -> psl::ValidatedSchema { + let datamodels = datamodels + .iter() + .map(|(file_name, dm)| (file_name.to_string(), psl::SourceFile::from(dm))) + .collect(); + + psl::validate_multi_file(datamodels) +} + +pub struct IntrospectionMultiTestResult { + /// Datamodels joined with file paths + pub datamodels: String, + /// The introspected data model is empty + pub is_empty: bool, + /// Introspection warnings + pub warnings: Option, + /// The database view definitions. None if preview feature + /// is not enabled. + pub views: Option>, +} + +pub struct IntrospectionTestResult { + /// Datamodel + pub datamodel: String, + /// The introspected data model is empty + pub is_empty: bool, + /// Introspection warnings + pub warnings: Option, + /// The database view definitions. None if preview feature + /// is not enabled. + pub views: Option>, +} + +pub trait ToIntrospectionTestResult { + fn to_single_test_result(self) -> IntrospectionTestResult; + fn to_multi_test_result(self) -> IntrospectionMultiTestResult; +} + +impl ToIntrospectionTestResult for IntrospectionResult { + fn to_single_test_result(self) -> IntrospectionTestResult { + IntrospectionTestResult::from(self) + } + + fn to_multi_test_result(self) -> IntrospectionMultiTestResult { + IntrospectionMultiTestResult::from(self) + } +} + +impl From for IntrospectionTestResult { + fn from(res: IntrospectionResult) -> Self { + Self { + datamodel: res.single_datamodel().to_string(), + is_empty: res.is_empty, + warnings: res.warnings, + views: res.views, + } + } +} + +impl From for IntrospectionMultiTestResult { + fn from(res: IntrospectionResult) -> Self { + let datamodels = res + .datamodels + .into_iter() + .sorted_unstable_by_key(|(file_name, _)| file_name.to_owned()) + .map(|(file_name, dm)| format!("// file: {file_name}\n{dm}")) + .join("------\n"); + + Self { + datamodels, + is_empty: res.is_empty, + warnings: res.warnings, + views: res.views, + } + } +} diff --git a/schema-engine/sql-introspection-tests/tests/re_introspection/mod.rs b/schema-engine/sql-introspection-tests/tests/re_introspection/mod.rs index 2da9377e69f4..a7c6e1897b95 100644 --- a/schema-engine/sql-introspection-tests/tests/re_introspection/mod.rs +++ b/schema-engine/sql-introspection-tests/tests/re_introspection/mod.rs @@ -1,4 +1,5 @@ mod mssql; +mod multi_file; mod mysql; mod postgresql; mod relation_mode; @@ -1561,7 +1562,7 @@ async fn re_introspecting_custom_compound_unique_upgrade(api: &mut TestApi) -> T let input_dm = indoc! {r#" model User { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) first Int last Int @@ -1571,7 +1572,7 @@ async fn re_introspecting_custom_compound_unique_upgrade(api: &mut TestApi) -> T let final_dm = indoc! {r#" model User { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) first Int last Int diff --git a/schema-engine/sql-introspection-tests/tests/re_introspection/multi_file.rs b/schema-engine/sql-introspection-tests/tests/re_introspection/multi_file.rs new file mode 100644 index 000000000000..63c627e84e96 --- /dev/null +++ b/schema-engine/sql-introspection-tests/tests/re_introspection/multi_file.rs @@ -0,0 +1,976 @@ +use barrel::types; +use sql_introspection_tests::test_api::*; + +fn with_config(dm: &str, config: String) -> String { + format!("{config}\n{dm}") +} + +// ----- Models ----- + +#[test_connector(exclude(CockroachDb))] +async fn reintrospect_new_model_single_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Unrelated", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Unrelated_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let config = &api.pure_config(); + let main_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [("main.prisma", format!("{config}\n{main_dm}",))]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + } + + model Unrelated { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(exclude(CockroachDb))] +async fn reintrospect_new_model_multi_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Post", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Post_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Unrelated", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Unrelated_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [ + ("user.prisma", with_config(user_dm, api.pure_config())), + ("post.prisma", post_dm.to_string()), + ]; + + let expected = expect![[r#" + // file: introspected.prisma + model Unrelated { + id Int @id @default(autoincrement()) + } + ------ + // file: post.prisma + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + Ok(()) +} + +#[test_connector(exclude(CockroachDb))] +async fn reintrospect_removed_model_single_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let config = &api.pure_config(); + let main_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + + model Removed { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [("main.prisma", format!("{config}\n{main_dm}",))]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(exclude(CockroachDb))] +async fn reintrospect_removed_model_multi_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Unrelated", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Unrelated_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [ + ("user.prisma", with_config(user_dm, api.pure_config())), + ("post.prisma", post_dm.to_string()), + ]; + + let expected = expect![[r#" + // file: introspected.prisma + model Unrelated { + id Int @id @default(autoincrement()) + } + ------ + // file: post.prisma + + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + Ok(()) +} + +// ----- Enums ----- + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_new_enum_single_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + api.raw_cmd(r#"CREATE TYPE "theEnumName" AS ENUM ('A', 'B');"#).await; + + let main_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [("main.prisma", with_config(main_dm, api.pure_config()))]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + } + + enum theEnumName { + A + B + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_removed_enum_single_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let main_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + + enum removedEnum { + A + B + } + "#}; + + let input_dms = [("main.prisma", with_config(main_dm, api.pure_config()))]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_new_enum_multi_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Post", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Post_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + api.raw_cmd(r#"CREATE TYPE "theEnumName" AS ENUM ('A', 'B');"#).await; + + let config = &api.pure_config(); + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [ + ("user.prisma", format!("{config}\n{user_dm}")), + ("post.prisma", post_dm.to_string()), + ]; + + let expected = expect![[r#" + // file: introspected.prisma + enum theEnumName { + A + B + } + ------ + // file: post.prisma + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_removed_enum_multi_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let config = &api.pure_config(); + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let enum_dm = indoc! {r#" + enum theEnumName { + A + B + } + "#}; + + let input_dms = [ + ("user.prisma", format!("{config}\n{user_dm}")), + ("enum.prisma", enum_dm.to_string()), + ]; + + let expected = expect![[r#" + // file: enum.prisma + + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +// ----- Views ----- + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn introspect_multi_view_preview_feature_is_required(api: &mut TestApi) -> TestResult { + let setup = indoc! {r#" + CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NULL + ); + + CREATE VIEW "Schwuser" AS + SELECT id, first_name, last_name FROM "User"; + "#}; + + api.raw_cmd(setup).await; + + let expected = expect![[r#" + // file: schema.prisma + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model User { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(255) + last_name String? @db.VarChar(255) + } + "#]]; + + api.expect_datamodels(&expected).await; + + api.expect_no_warnings().await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(Postgres16), exclude(CockroachDb), preview_features("views"))] +// the expect_view_definition is slightly different than for Postgres16 +async fn reintrospect_new_view_single_file(api: &mut TestApi) -> TestResult { + let setup = indoc! {r#" + CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NULL + ); + + CREATE VIEW "Schwuser" AS + SELECT id, first_name, last_name FROM "User"; + "#}; + + api.raw_cmd(setup).await; + + let main_dm = with_config( + indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}, + api.pure_config(), + ); + let input_dms = [("main.prisma", main_dm)]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(255) + last_name String? @db.VarChar(255) + } + + /// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. + view Schwuser { + id Int? + first_name String? @db.VarChar(255) + last_name String? @db.VarChar(255) + + @@ignore + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + let expected = expect![[r#" + SELECT + "User".id, + "User".first_name, + "User".last_name + FROM + "User";"#]]; + + api.expect_view_definition("Schwuser", &expected).await; + + let expected = expect![[r#" + *** WARNING *** + + The following views were ignored as they do not have a valid unique identifier or id. This is currently not supported by Prisma Client. Please refer to the documentation on defining unique identifiers in views: https://pris.ly/d/view-identifiers + - "Schwuser" + "#]]; + api.expect_warnings(&expected).await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(Postgres16), exclude(CockroachDb), preview_features("views"))] +// the expect_view_definition is slightly different than for Postgres16 +async fn reintrospect_removed_view_single_file(api: &mut TestApi) -> TestResult { + let setup = indoc! {r#" + CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NULL + ); + "#}; + + api.raw_cmd(setup).await; + + let main_dm = with_config( + indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + + view RemovedView { + id Int? + first_name String? @db.VarChar(255) + last_name String? @db.VarChar(255) + + @@ignore + } + "#}, + api.pure_config(), + ); + let input_dms = [("main.prisma", main_dm)]; + + let expected = expect![[r#" + // file: main.prisma + model User { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(255) + last_name String? @db.VarChar(255) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + let expected = expect![""]; + api.expect_warnings(&expected).await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(Postgres16), exclude(CockroachDb), preview_features("views"))] +// the expect_view_definition is slightly different than for Postgres16 +async fn reintrospect_new_view_multi_file(api: &mut TestApi) -> TestResult { + let setup = indoc! {r#" + CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NULL + ); + + CREATE TABLE "Post" ( + id SERIAL PRIMARY KEY + ); + + CREATE VIEW "Schwuser" AS + SELECT id, first_name, last_name FROM "User"; + "#}; + + api.raw_cmd(setup).await; + + let user_dm = with_config( + indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}, + api.pure_config(), + ); + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + let input_dms = [("user.prisma", user_dm), ("post.prisma", post_dm.to_string())]; + + let expected = expect![[r#" + // file: introspected.prisma + /// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client. + view Schwuser { + id Int? + first_name String? @db.VarChar(255) + last_name String? @db.VarChar(255) + + @@ignore + } + ------ + // file: post.prisma + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(255) + last_name String? @db.VarChar(255) + } + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + let expected = expect![[r#" + SELECT + "User".id, + "User".first_name, + "User".last_name + FROM + "User";"#]]; + + api.expect_view_definition_multi("Schwuser", &expected).await; + + let expected = expect![[r#" + *** WARNING *** + + The following views were ignored as they do not have a valid unique identifier or id. This is currently not supported by Prisma Client. Please refer to the documentation on defining unique identifiers in views: https://pris.ly/d/view-identifiers + - "Schwuser" + "#]]; + api.expect_warnings(&expected).await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(Postgres16), exclude(CockroachDb), preview_features("views"))] +// the expect_view_definition is slightly different than for Postgres16 +async fn reintrospect_removed_view_multi_file(api: &mut TestApi) -> TestResult { + let setup = indoc! {r#" + CREATE TABLE "User" ( + id SERIAL PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NULL + ); + "#}; + + api.raw_cmd(setup).await; + + let user_dm = with_config( + indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}, + api.pure_config(), + ); + let view_dm = indoc! {r#" + view Schwuser { + id Int? + first_name String? @db.VarChar(255) + last_name String? @db.VarChar(255) + + @@ignore + } + "#}; + let input_dms = [("user.prisma", user_dm), ("view.prisma", view_dm.to_string())]; + + let expected = expect![[r#" + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + first_name String @db.VarChar(255) + last_name String? @db.VarChar(255) + } + ------ + // file: view.prisma + + "#]]; + + api.expect_re_introspected_datamodels(&input_dms, expected).await; + + let expected = expect![""]; + api.expect_warnings(&expected).await; + + Ok(()) +} + +// ----- Configuration ----- +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_keep_configuration_in_same_file(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Post", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Post_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let expected = expect![[r#" + // file: post.prisma + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", with_config(user_dm, api.pure_config())), + ("post.prisma", post_dm.to_string()), + ], + expected, + ) + .await; + + let expected = expect![[r#" + // file: post.prisma + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", user_dm.to_string()), + ("post.prisma", with_config(post_dm, api.pure_config())), + ], + expected, + ) + .await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_keep_configuration_when_spread_across_files(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + + migration.create_table("Post", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("Post_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let expected = expect![[r#" + // file: post.prisma + generator client { + provider = "prisma-client-js" + } + + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", format!("{}\n{user_dm}", api.datasource_block_string())), + ("post.prisma", format!("{}\n{post_dm}", api.generator_block_string())), + ], + expected, + ) + .await; + + let expected = expect![[r#" + // file: post.prisma + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model Post { + id Int @id @default(autoincrement()) + } + ------ + // file: user.prisma + generator client { + provider = "prisma-client-js" + } + + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", format!("{}\n{user_dm}", api.generator_block_string())), + ("post.prisma", format!("{}\n{post_dm}", api.datasource_block_string())), + ], + expected, + ) + .await; + + Ok(()) +} + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_keep_configuration_when_no_models(api: &mut TestApi) -> TestResult { + api.barrel() + .execute(|migration| { + migration.create_table("User", |t| { + t.add_column("id", types::integer().increments(true)); + t.add_constraint("User_pkey", types::primary_constraint(vec!["id"])); + }); + }) + .await?; + + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let expected = expect![[r#" + // file: post.prisma + generator client { + provider = "prisma-client-js" + } + ------ + // file: user.prisma + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", format!("{}\n{user_dm}", api.datasource_block_string())), + ("post.prisma", format!("{}\n{post_dm}", api.generator_block_string())), + ], + expected, + ) + .await; + + let expected = expect![[r#" + // file: post.prisma + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + ------ + // file: user.prisma + generator client { + provider = "prisma-client-js" + } + + model User { + id Int @id @default(autoincrement()) + } + "#]]; + + api.expect_re_introspected_datamodels_with_config( + &[ + ("user.prisma", format!("{}\n{user_dm}", api.generator_block_string())), + ("post.prisma", format!("{}\n{post_dm}", api.datasource_block_string())), + ], + expected, + ) + .await; + + Ok(()) +} + +// ----- Miscellaneous ----- + +#[test_connector(tags(Postgres), exclude(CockroachDb))] +async fn reintrospect_empty_multi_file(api: &mut TestApi) -> TestResult { + let user_dm = indoc! {r#" + model User { + id Int @id @default(autoincrement()) + } + "#}; + let post_dm = indoc! {r#" + model Post { + id Int @id @default(autoincrement()) + } + "#}; + + let input_dms = [ + ("user.prisma", with_config(user_dm, api.pure_config())), + ("post.prisma", post_dm.to_string()), + ]; + + let expected = expect![[r#" + // file: post.prisma + + ------ + // file: user.prisma + generator client { + provider = "prisma-client-js" + } + + datasource db { + provider = "postgresql" + url = "env(TEST_DATABASE_URL)" + } + "#]]; + + api.expect_re_introspected_datamodels_with_config(&input_dms, expected) + .await; + + Ok(()) +} diff --git a/schema-engine/sql-introspection-tests/tests/simple.rs b/schema-engine/sql-introspection-tests/tests/simple.rs index 1eb641a83d1a..d79ddffe91e8 100644 --- a/schema-engine/sql-introspection-tests/tests/simple.rs +++ b/schema-engine/sql-introspection-tests/tests/simple.rs @@ -4,7 +4,7 @@ use indoc::formatdoc; use psl::PreviewFeature; use quaint::single::Quaint; use schema_connector::{CompositeTypeDepth, ConnectorParams, IntrospectionContext, SchemaConnector}; -use sql_introspection_tests::test_api::Queryable; +use sql_introspection_tests::test_api::{Queryable, ToIntrospectionTestResult}; use sql_schema_connector::SqlSchemaConnector; use std::{fs, io::Write as _, path}; use test_setup::{ @@ -210,8 +210,9 @@ source .test_database_urls/mysql_5_6 let ctx = IntrospectionContext::new(psl, CompositeTypeDepth::Infinite, namespaces.clone()); let introspected = tok(api.introspect(&ctx)) + .map(ToIntrospectionTestResult::to_single_test_result) .unwrap_or_else(|err| panic!("{}", err)) - .data_model; + .datamodel; let last_comment_idx = text .match_indices("/*") @@ -237,8 +238,9 @@ source .test_database_urls/mysql_5_6 let ctx = IntrospectionContext::new(introspected_schema, CompositeTypeDepth::Infinite, namespaces); tok(api.introspect(&ctx)) + .map(ToIntrospectionTestResult::to_single_test_result) .unwrap_or_else(|err| panic!("{}", err)) - .data_model + .datamodel }; if introspected == re_introspected {