Skip to content

Commit

Permalink
feat(schema-cli): multi-file introspection (#4892)
Browse files Browse the repository at this point in the history
  • Loading branch information
Weakky authored May 31, 2024
1 parent 9ee4383 commit 3a14d17
Show file tree
Hide file tree
Showing 22 changed files with 440 additions and 98 deletions.
21 changes: 16 additions & 5 deletions libs/test-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,11 @@ async fn main() -> anyhow::Result<()> {
unreachable!()
};

let base_directory_path = file_path
.as_ref()
.map(|p| std::path::Path::new(p).parent().unwrap().to_string_lossy().to_string())
.unwrap_or_else(|| "/".to_string());

let api = schema_core::schema_api(Some(schema.clone()), None)?;

let params = IntrospectParams {
Expand All @@ -213,14 +218,15 @@ async fn main() -> anyhow::Result<()> {
content: schema,
}],
},
base_directory_path,
force: false,
composite_type_depth: composite_type_depth.unwrap_or(0),
namespaces: None,
};

let introspected = api.introspect(params).await.map_err(|err| anyhow::anyhow!("{err:?}"))?;
let mut introspected = api.introspect(params).await.map_err(|err| anyhow::anyhow!("{err:?}"))?;

println!("{}", &introspected.datamodel);
println!("{}", &introspected.schema.files.remove(0).content);
}
Command::ValidateDatamodel(cmd) => {
use std::io::Read as _;
Expand Down Expand Up @@ -328,14 +334,17 @@ async fn generate_dmmf(cmd: &DmmfCommand) -> anyhow::Result<()> {
let skeleton = minimal_schema_from_url(url)?;

let api = schema_core::schema_api(Some(skeleton.clone()), None)?;
let base_path_directory = "/tmp";
let path = "/tmp/prisma-test-cli-introspected.prisma";

let params = IntrospectParams {
schema: SchemasContainer {
files: vec![SchemaContainer {
path: "schema.prisma".to_string(),
path: path.to_string(),
content: skeleton,
}],
},
base_directory_path: base_path_directory.to_string(),
force: false,
composite_type_depth: -1,
namespaces: None,
Expand All @@ -345,8 +354,10 @@ async fn generate_dmmf(cmd: &DmmfCommand) -> anyhow::Result<()> {

eprintln!("{}", "Schema was successfully introspected from database URL".green());

let path = "/tmp/prisma-test-cli-introspected.prisma";
std::fs::write(path, introspected.datamodel)?;
for schema in introspected.schema.files {
std::fs::write(schema.path, schema.content)?;
}

path.to_owned()
} else if let Some(file_path) = cmd.file_path.as_ref() {
file_path.clone()
Expand Down
7 changes: 6 additions & 1 deletion psl/parser-database/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub use ids::*;
pub use names::is_reserved_type_name;
use names::Names;
pub use relations::{ManyToManyRelationId, ReferentialAction, RelationId};
use schema_ast::ast::SourceConfig;
use schema_ast::ast::{GeneratorConfig, SourceConfig};
pub use schema_ast::{ast, SourceFile};
pub use types::{
IndexAlgorithm, IndexFieldPath, IndexType, OperatorClass, RelationFieldId, ScalarFieldId, ScalarFieldType,
Expand Down Expand Up @@ -242,6 +242,11 @@ impl ParserDatabase {
pub fn datasources(&self) -> impl Iterator<Item = &SourceConfig> {
self.iter_asts().flat_map(|ast| ast.sources())
}

/// Iterate all generators defined in the schema
pub fn generators(&self) -> impl Iterator<Item = &GeneratorConfig> {
self.iter_asts().flat_map(|ast| ast.generators())
}
}

impl std::ops::Index<FileId> for ParserDatabase {
Expand Down
194 changes: 155 additions & 39 deletions schema-engine/cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,40 @@ impl TestApi {
}
}

macro_rules! write_multi_file_vec {
// Match multiple pairs of filename and content
( $( $filename:expr => $content:expr ),* $(,)? ) => {
{
use std::fs::File;
use std::io::Write;

// Create a result vector to collect errors
let mut results = Vec::new();
let tmpdir = tempfile::tempdir().unwrap();

fs::create_dir_all(&tmpdir).unwrap();

$(
let file_path = tmpdir.path().join($filename);
// Attempt to create or open the file
let result = (|| -> std::io::Result<()> {
let mut file = File::create(&file_path)?;
file.write_all($content.as_bytes())?;
Ok(())
})();

result.unwrap();

// Push the result of the operation to the results vector
results.push((file_path.to_string_lossy().into_owned(), $content));
)*

// Return the results vector for further inspection if needed
(tmpdir, results)
}
};
}

#[test_connector(tags(Mysql))]
fn test_connecting_with_a_working_mysql_connection_string(api: TestApi) {
let connection_string = api.connection_string();
Expand Down Expand Up @@ -445,6 +479,7 @@ fn introspect_sqlite_empty_database() {
"schema": { "files": [{ "path": "schema.prisma", "content": schema }] },
"force": true,
"compositeTypeDepth": 5,
"baseDirectoryPath": "./base_directory_path/"
}
}))
.unwrap();
Expand Down Expand Up @@ -492,6 +527,7 @@ fn introspect_sqlite_invalid_empty_database() {
"schema": { "files": [{ "path": "schema.prisma", "content": schema }] },
"force": true,
"compositeTypeDepth": -1,
"baseDirectoryPath": "./base_directory_path/"
}
}))
.unwrap();
Expand Down Expand Up @@ -566,7 +602,7 @@ fn execute_postgres(api: TestApi) {
}

#[test_connector(tags(Postgres), exclude(CockroachDb), preview_features("views"))]
fn introspect_postgres(api: TestApi) {
fn introspect_single_postgres_force(api: TestApi) {
/* Drop and create database via `drop-database` and `create-database` */

let connection_string = api.connection_string();
Expand All @@ -590,7 +626,7 @@ fn introspect_postgres(api: TestApi) {
}
"#};

let schema_path = tmpdir.path().join("prisma.schema");
let schema_path = tmpdir.path().join("schema.prisma");
fs::write(&schema_path, schema).unwrap();

let command = Command::new(schema_engine_bin_path());
Expand Down Expand Up @@ -645,9 +681,10 @@ fn introspect_postgres(api: TestApi) {
"method": "introspect",
"id": 1,
"params": {
"schema": { "files": [{ "path": &schema_path, "content": &schema }] },
"schema": { "files": [{ "path": "./prisma/schema.prisma", "content": &schema }] },
"force": true,
"compositeTypeDepth": 5,
"baseDirectoryPath": "./base_directory_path/"
}
}))
.unwrap();
Expand All @@ -659,7 +696,119 @@ fn introspect_postgres(api: TestApi) {
stdout.read_line(&mut response).unwrap();

let expected = expect![[r#"
{"jsonrpc":"2.0","result":{"datamodel":"generator js {\n provider = \"prisma-client-js\"\n previewFeatures = [\"views\"]\n}\n\ndatasource db {\n provider = \"postgres\"\n url = env(\"TEST_DATABASE_URL\")\n}\n\nmodel A {\n id Int @id @default(autoincrement())\n data String?\n}\n\n/// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.\nview B {\n col Int?\n\n @@ignore\n}\n","views":[{"definition":"SELECT\n 1 AS col;","name":"B","schema":"public"}],"warnings":"*** WARNING ***\n\nThe 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\n - \"B\"\n"},"id":1}
{"jsonrpc":"2.0","result":{"schema":{"files":[{"content":"generator js {\n provider = \"prisma-client-js\"\n previewFeatures = [\"views\"]\n}\n\ndatasource db {\n provider = \"postgres\"\n url = env(\"TEST_DATABASE_URL\")\n}\n\nmodel A {\n id Int @id @default(autoincrement())\n data String?\n}\n\n/// The underlying view does not contain a valid unique identifier and can therefore currently not be handled by Prisma Client.\nview B {\n col Int?\n\n @@ignore\n}\n","path":"./prisma/schema.prisma"}]},"views":[{"definition":"SELECT\n 1 AS col;","name":"B","schema":"public"}],"warnings":"*** WARNING ***\n\nThe 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\n - \"B\"\n"},"id":1}
"#]];

expected.assert_eq(&response);
});
}

#[test_connector(tags(Postgres), exclude(CockroachDb), preview_features("views"))]
fn introspect_multi_postgres_force(api: TestApi) {
/* Drop and create database via `drop-database` and `create-database` */

let connection_string = api.connection_string();

let output = api.run(&["--datasource", &connection_string, "drop-database"]);
assert!(output.status.success(), "{output:#?}");

let output = api.run(&["--datasource", &connection_string, "create-database"]);
assert!(output.status.success(), "{output:#?}");

let (tmpdir, files) = write_multi_file_vec! {
"a.prisma" => r#"
datasource db {
provider = "postgres"
url = env("TEST_DATABASE_URL")
}
"#,
"b.prisma" => r#"
model User {
id Int @id
}
"#,
};

let files = files
.into_iter()
.map(|(schema_path, content)| SchemaContainer {
path: schema_path,
content: content.to_string(),
})
.collect::<Vec<_>>();

for file in &files {
fs::write(&file.path, &file.content).unwrap();
}

let command = Command::new(schema_engine_bin_path());

with_child_process(command, |process| {
let stdin = process.stdin.as_mut().unwrap();
let mut stdout = BufReader::new(process.stdout.as_mut().unwrap());

/* Create table via `dbExecute` */

let script = indoc! {r#"
DROP TABLE IF EXISTS "public"."A";
DROP VIEW IF EXISTS "public"."B";
CREATE TABLE "public"."A" (
id SERIAL PRIMARY KEY,
data TEXT
);
CREATE VIEW "public"."B" AS SELECT 1 AS col;
"#};

let msg = serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0",
"method": "dbExecute",
"id": 1,
"params": {
"datasourceType": {
"tag": "schema",
"files": files,
"configDir": tmpdir.path().to_string_lossy().to_string(),
},
"script": script,
}
}))
.unwrap();
stdin.write_all(msg.as_bytes()).unwrap();
stdin.write_all(b"\n").unwrap();

let mut response = String::new();
stdout.read_line(&mut response).unwrap();

let expected = expect![[r#"
{"jsonrpc":"2.0","result":null,"id":1}
"#]];

expected.assert_eq(&response);

/* Introspect via `introspect` */
let msg = serde_json::to_string(&serde_json::json!({
"jsonrpc": "2.0",
"method": "introspect",
"id": 1,
"params": {
"schema": { "files": files },
"force": true,
"compositeTypeDepth": 5,
"baseDirectoryPath": "./base_directory_path/"
}
}))
.unwrap();

stdin.write_all(msg.as_bytes()).unwrap();
stdin.write_all(b"\n").unwrap();

let mut response = String::new();
stdout.read_line(&mut response).unwrap();

let expected = expect![[r#"
{"jsonrpc":"2.0","result":{"schema":{"files":[{"content":"datasource db {\n provider = \"postgres\"\n url = env(\"TEST_DATABASE_URL\")\n}\n\nmodel A {\n id Int @id @default(autoincrement())\n data String?\n}\n","path":"./base_directory_path/introspected.prisma"}]},"views":null,"warnings":null},"id":1}
"#]];

expected.assert_eq(&response);
Expand Down Expand Up @@ -700,6 +849,7 @@ fn introspect_e2e() {
"schema": schema,
"force": true,
"compositeTypeDepth": 5,
"baseDirectoryPath": "./base_directory_path/",
}
}))
.unwrap();
Expand All @@ -715,40 +865,6 @@ fn introspect_e2e() {
});
}

macro_rules! write_multi_file_vec {
// Match multiple pairs of filename and content
( $( $filename:expr => $content:expr ),* $(,)? ) => {
{
use std::fs::File;
use std::io::Write;

// Create a result vector to collect errors
let mut results = Vec::new();
let tmpdir = tempfile::tempdir().unwrap();

fs::create_dir_all(&tmpdir).unwrap();

$(
let file_path = tmpdir.path().join($filename);
// Attempt to create or open the file
let result = (|| -> std::io::Result<()> {
let mut file = File::create(&file_path)?;
file.write_all($content.as_bytes())?;
Ok(())
})();

result.unwrap();

// Push the result of the operation to the results vector
results.push((file_path.to_string_lossy().into_owned(), $content));
)*

// Return the results vector for further inspection if needed
results
}
};
}

fn to_schema_containers(files: Vec<(String, &str)>) -> Vec<SchemaContainer> {
files
.into_iter()
Expand All @@ -767,7 +883,7 @@ fn to_schemas_container(files: Vec<(String, &str)>) -> SchemasContainer {

#[test_connector(tags(Postgres))]
fn get_database_version_multi_file(_api: TestApi) {
let files = write_multi_file_vec! {
let (_, files) = write_multi_file_vec! {
"a.prisma" => r#"
datasource db {
provider = "postgres"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -413,20 +413,20 @@ impl<'a> Statistics<'a> {

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(),
Some(walker) => Cow::Borrowed(ctx.previous_schema().db.file_name(walker.file_id())),
None => ctx.introspection_file_path(),
};

rendered.push_composite_type(Cow::Borrowed(file_name), r#type);
rendered.push_composite_type(file_name, r#type);
}

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(),
Some(walker) => Cow::Borrowed(ctx.previous_schema().db.file_name(walker.file_id())),
None => ctx.introspection_file_path(),
};

rendered.push_model(Cow::Borrowed(file_name), model);
rendered.push_model(file_name, model);
}
}

Expand Down
Loading

0 comments on commit 3a14d17

Please sign in to comment.