From ca80784c58b04f314c7551f9f6cc990680725383 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Sat, 13 Jan 2024 19:45:22 +0900 Subject: [PATCH 01/11] First WIP --- src/migrations/create.rs | 285 ++++++++++++++++++++++++++++++++++----- 1 file changed, 254 insertions(+), 31 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 5aaf419c8..6c8d6023c 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -1,6 +1,97 @@ -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::{ + borrow::Cow, + collections::{HashMap, BTreeMap}, + path::{Path, PathBuf}, sync::Arc +}; + +#[derive(Clone, Debug, Default)] +pub struct InteractiveMigrationInfo { + cast_info: HashMap>, + function_info: Vec, + properties: PropertyInfo +} + +#[derive(Queryable, Debug, Clone, Default)] +pub struct PropertyInfo { + regular_properties: Vec, + link_properties: Vec +} + +#[derive(Queryable, Debug, Clone)] +pub struct FunctionInfo { + name: String, + input: String, + returns: String +} + +// Returns all functions as long as the type returned input type does not equal output +// (Casts only required when changing to a new type) +// Currently returns 161 functions +async fn get_function_info(cli: &mut Connection) -> Result, Error> { + cli.query::( + r#"with fn := (select schema::Function filter count(.params.type.name) = 1), + select fn { + name, + input := array_join(array_agg((.params.type.name)), ''), + returns := .return_type.name + };"#, &()).await +} + +#[derive(Queryable, Debug, Clone)] +pub struct CastInfo { + from_type_name: String, + to_type_name: String +} + +async fn get_cast_info(cli: &mut Connection) -> Result>, Error> { + let cast_info: Vec = cli.query( + "select schema::Cast { + from_type_name := .from_type.name, + to_type_name := .to_type.name + };", &()).await?; + let mut map = std::collections::HashMap::new(); + for cast in cast_info { + map.entry(cast.from_type_name).or_insert(Vec::new()).push(cast.to_type_name); + } + Ok(map) +} + +// If regular_properties has the same link name at least twice and the name matches one inside +// link_properties, then there is both a regular and a link property of the same name +async fn get_all_property_info(cli: &mut Connection) -> Result { + cli.query_required_single("with + + links := (select schema::Link filter .builtin = false), + link_names := (select names := links.properties.name filter names not in {'__type__', 'target', 'source'}), + + properties := (select schema::Property filter .builtin = false), + property_names := (select names := properties.name filter names not in {'id', 'target', 'source'}), + + select {regular_properties := property_names, link_properties := distinct link_names, };", &()).await +} + +// Don't want to fail CLI if migration info can't be found, just log and return default +async fn get_migration_info(cli: &mut Connection) -> InteractiveMigrationInfo { + let function_info = get_function_info(cli).await.unwrap_or_else(|e| { + println!("Failed to find function info: {e}"); + log::debug!("Failed to find function info: {e}"); + Default::default() + }); + let cast_info = get_cast_info(cli).await.unwrap_or_else(|e| { + log::debug!("Failed to find cast_info: {e}"); + Default::default() + }); + let properties = get_all_property_info(cli).await.unwrap_or_else(|e| { + log::debug!("Failed to find property info: {e}"); + Default::default() + }); + + InteractiveMigrationInfo { + cast_info, + function_info, + properties + } +} use anyhow::Context as _; use colorful::Colorful; @@ -60,6 +151,20 @@ enum Choice { Quit, } + +/// Example: +/// +/// "required_user_input": [ +/// { +/// "prompt": "Please specify a conversion expression to alter the type of property 'strength'", +/// "new_type": "std::str", +/// "old_type": "std::int64", +/// "placeholder": "cast_expr", +/// "pointer_name": "strength", +/// "new_type_is_object": false, +/// "old_type_is_object": false +/// } +/// ] #[derive(Deserialize, Debug, Clone)] pub struct RequiredUserInput { placeholder: String, @@ -79,6 +184,17 @@ pub struct StatementProposal { pub text: String, } +// Example: +// +// "proposed": { +// "prompt": "did you alter the type of property 'strength' of link 'times'?", +// "data_safe": false, +// "prompt_id": "SetPropertyType PROPERTY default::__|strength@default|__||times&default||Time", +// "confidence": 1.0, +// "statements": [ +// { +// "text": "ALTER TYPE default::Time {\n ALTER LINK times {\n ALTER PROPERTY strength {\n SET TYPE std::str USING (\\(cast_expr));\n };\n };\n};" +// } #[derive(Deserialize, Debug)] pub struct Proposal { pub prompt_id: Option, @@ -90,6 +206,36 @@ pub struct Proposal { pub required_user_input: Vec, } +// Returned from each DESCRIBE CURRENT SCHEMA AS JSON during a migration. +// e.g. +// +// { +// "parent": "m17emwiottbbfc4coo7ybcrkljr5bhdv46ouoyyjrsj4qwvg7w5ina", +// "complete": false, +// "proposed": { +// "prompt": "did you alter the type of property 'strength' of link 'times'?", +// "data_safe": false, +// "prompt_id": "SetPropertyType PROPERTY default::__|strength@default|__||times&default||Time", +// "confidence": 1.0, +// "statements": [ +// { +// "text": "ALTER TYPE default::Time {\n ALTER LINK times {\n ALTER PROPERTY strength {\n SET TYPE std::str USING (\\(cast_expr));\n };\n };\n};" +// } +// ], +// "required_user_input": [ +// { +// "prompt": "Please specify a conversion expression to alter the type of property 'strength'", +// "new_type": "std::str", +// "old_type": "std::int64", +// "placeholder": "cast_expr", +// "pointer_name": "strength", +// "new_type_is_object": false, +// "old_type_is_object": false +// } +// ] +// }, +// "confirmed": [] +// } #[derive(Deserialize, Queryable, Debug)] #[edgedb(json)] pub struct CurrentMigration { @@ -267,6 +413,7 @@ pub async fn execute_start_migration(ctx: &Context, cli: &mut Connection) -> anyhow::Result<()> { let (text, source_map) = gen_start_migration(&ctx).await?; + match execute(cli, text).await { Ok(_) => Ok(()), Err(e) if e.is::() => { @@ -347,9 +494,84 @@ pub fn make_default_expression(input: &RequiredUserInput) Some(expr) } -pub async fn unsafe_populate(_ctx: &Context, cli: &mut Connection) - -> anyhow::Result +pub fn make_default_expression_interactive(input: &RequiredUserInput, info: Arc) -> Option { + let name = &input.placeholder[..]; + let kind_end = name.find("_expr").unwrap_or(name.len()); + println!("Function info: {:#?}", info.function_info); + let expr = match &name[..kind_end] { + "fill" if input.type_name.is_some() => { + format!("<{}>{{}}", + input.type_name.as_ref().unwrap()) + } + // Please specify a conversion expression to alter the type of property 'd': + // cast_expr> .d + "cast" if input.pointer_name.is_some() && input.new_type.is_some() => { + let pointer_name = input.pointer_name.as_deref().unwrap(); + // Sometimes types will show up eg. as array for some reason instead of array + let old_type = input.old_type.as_deref().unwrap_or_default().replace('|', "::"); + let new_type = input.new_type.as_deref().unwrap_or_default().replace('|', "::"); + println!("Old type: {old_type} New type: {new_type}"); + match (input.old_type_is_object, input.new_type_is_object) { + (Some(true), Some(true)) => { + format!(".{pointer_name}[IS {new_type}]") + } + (Some(false), Some(false)) | (None, None) => { + // Check if old type has any casts + if let Some(vals) = info.cast_info.get(&old_type) { + // Then see if any of the available casts match the new type + if vals.iter().any(|t| t == &new_type) { + + // cast_expr> @strength + if info.properties.link_properties.iter().any(|l| *l == pointer_name) { + if info.properties.regular_properties.iter().filter(|l| *l == pointer_name).count() > 1 { + println!("Note: `{pointer_name}` is the name of both a regular property and a link property."); + println!("Change the _ below to . if the cast is for a regular property, or to @ if the cast is for a link property.\n"); + format!("<{new_type}>_{pointer_name}") + } else { + // Definitely just a link property + format!("<{new_type}>@{pointer_name}") + } + } else { + format!("<{new_type}>.{pointer_name}") + } + } else { + let available_functions = info.function_info.iter().filter(|c| { + // How to check for anytype, etc.? e.g. suggest len() for any array when changing to int64 + c.input.contains(&old_type.to_string()) && c.returns.contains(&new_type) + }).collect::>(); + if !available_functions.is_empty() { + println!("Available functions: {available_functions:#?}"); + println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); + for function in available_functions { + let FunctionInfo { name, input, returns } = function; + println!("{name}({input}) -> {returns}"); + } + } + format!(".{pointer_name}") + } + + } else { + format!(".{pointer_name}") + } + } + // TODO(tailhook) maybe create something for mixed case? + _ => return None, + } + } + "conv" if input.pointer_name.is_some() => { + format!("(SELECT .{} LIMIT 1)", + input.pointer_name.as_ref().unwrap()) + } + _ => { + return None; + } + }; + Some(expr) +} + +pub async fn unsafe_populate(_ctx: &Context, cli: &mut Connection) -> anyhow::Result { + loop { let data = query_row::(cli, "DESCRIBE CURRENT MIGRATION AS JSON" @@ -445,20 +667,20 @@ async fn non_interactive_populate(_ctx: &Context, cli: &mut Connection) eprintln!("EdgeDB intended to apply the following migration:"); for statement in proposal.statements { for line in statement.text.lines() { - eprintln!(" {}", line); + eprintln!(" {line}"); } } eprintln!("But confidence is {}, below minimum threshold of {}", proposal.confidence, SAFE_CONFIDENCE); anyhow::bail!("EdgeDB is unable to make a decision. Please run in \ interactive mode to confirm changes, \ - or use `--allow-unsafe`"); + or use `--allow-unsafe` to automatically force all suggested changes"); } } else { anyhow::bail!("EdgeDB could not resolve \ migration automatically. Please run in \ interactive mode to confirm changes, \ - or use `--allow-unsafe`"); + or use `--allow-unsafe` to automatically force all suggested changes"); } } } @@ -480,14 +702,15 @@ async fn run_non_interactive(ctx: &Context, cli: &mut Connection, Ok(FutureMigration::new(key, descr)) } -impl InteractiveMigration<'_> { - fn new(cli: &mut Connection) -> InteractiveMigration { - InteractiveMigration { +impl<'a> InteractiveMigration<'a> { + async fn new(cli: &'a mut Connection) -> Result { + + Ok(InteractiveMigration { cli, save_point: 0, operations: vec![Set::new()], confirmed: Vec::new(), - } + }) } async fn save_point(&mut self) -> Result<(), Error> { execute(self.cli, @@ -499,7 +722,7 @@ impl InteractiveMigration<'_> { "ROLLBACK TO SAVEPOINT migration_{}", self.save_point) ).await } - async fn run(mut self) -> anyhow::Result { + async fn run(mut self, info: Arc) -> anyhow::Result { self.save_point().await?; loop { let descr = query_row::(self.cli, @@ -510,7 +733,7 @@ impl InteractiveMigration<'_> { return Ok(descr); } if let Some(proposal) = &descr.proposed { - match self.process_proposal(proposal).await { + match self.process_proposal(proposal, Arc::clone(&info)).await { Err(e) if e.is::() => return Ok(descr), Err(e) => return Err(e), Ok(()) => {} @@ -520,9 +743,7 @@ impl InteractiveMigration<'_> { } } } - async fn process_proposal(&mut self, proposal: &Proposal) - -> anyhow::Result<()> - { + async fn process_proposal(&mut self, proposal: &Proposal, info: Arc) -> anyhow::Result<()> { use Choice::*; let cur_oper = self.operations.last().unwrap(); @@ -540,7 +761,7 @@ impl InteractiveMigration<'_> { } println!("(approved as part of an earlier prompt)"); let input = self.cli.ping_while( - get_user_input(&proposal.required_user_input) + get_user_input(&proposal.required_user_input, Arc::clone(&info)) ).await; match input { Ok(data) => break data, @@ -563,7 +784,7 @@ impl InteractiveMigration<'_> { match self.cli.ping_while(choice(prompt)).await? { Yes => { let input_res = self.cli.ping_while( - get_user_input(&proposal.required_user_input) + get_user_input(&proposal.required_user_input, Arc::clone(&info)) ).await; match input_res { Ok(data) => input = data, @@ -573,9 +794,7 @@ impl InteractiveMigration<'_> { break; } No => { - execute(self.cli, - "ALTER CURRENT MIGRATION REJECT PROPOSED" - ).await?; + execute(self.cli, "ALTER CURRENT MIGRATION REJECT PROPOSED").await?; self.save_point += 1; self.save_point().await?; return Ok(()); @@ -670,10 +889,11 @@ impl InteractiveMigration<'_> { async fn run_interactive(_ctx: &Context, cli: &mut Connection, - key: MigrationKey, options: &CreateMigration) + key: MigrationKey, options: &CreateMigration, info: Arc) -> anyhow::Result { - let descr = InteractiveMigration::new(cli).run().await?; + + let descr = InteractiveMigration::new(cli).await?.run(info).await?; if descr.confirmed.is_empty() && !options.allow_empty { print::warn("No schema changes detected."); @@ -690,11 +910,12 @@ pub async fn write_migration(ctx: &Context, descr: &impl MigrationToText, let filename = match &descr.key() { MigrationKey::Index(idx) => { let dir = ctx.schema_dir.join("migrations"); - dir.join(format!("{:05}.edgeql", idx)) + dir.join(format!("{idx:05}.edgeql")) } MigrationKey::Fixup { target_revision } => { let dir = ctx.schema_dir.join("fixups"); - dir.join(format!("{}-{}.edgeql", descr.parent()?, target_revision)) + let parent = descr.parent()?; + dir.join(format!("{parent}-{target_revision}.edgeql")) } }; _write_migration(descr, filename.as_ref(), verbose).await @@ -800,6 +1021,7 @@ pub async fn normal_migration(cli: &mut Connection, ctx: &Context, create: &CreateMigration) -> anyhow::Result { + let info = Arc::new(get_migration_info(cli).await); execute_start_migration(&ctx, cli).await?; async_try! { async { @@ -824,7 +1046,7 @@ pub async fn normal_migration(cli: &mut Connection, ctx: &Context, log::warn!("The `--allow-unsafe` flag is unused \ in interactive mode"); } - run_interactive(&ctx, cli, key, &create).await + run_interactive(&ctx, cli, key, &create, info).await } }, finally async { @@ -850,9 +1072,9 @@ fn add_newline_after_comment(value: &mut String) -> Result<(), anyhow::Error> { Ok(()) } -fn get_input(req: &RequiredUserInput) -> Result { +fn get_input(req: &RequiredUserInput, info: Arc) -> Result { let prompt = format!("{}> ", req.placeholder); - let mut prev = make_default_expression(req).unwrap_or(String::new()); + let mut prev = make_default_expression_interactive(req, info).unwrap_or_default(); loop { println!("{}:", req.prompt); let mut value = match prompt::expression(&prompt, &req.placeholder, &prev) { @@ -876,13 +1098,14 @@ fn get_input(req: &RequiredUserInput) -> Result { } } -async fn get_user_input(req: &[RequiredUserInput]) +async fn get_user_input(req: &[RequiredUserInput], info: Arc) -> Result, anyhow::Error> { let mut result = BTreeMap::new(); for item in req { let copy = item.clone(); - let input = unblock(move || get_input(©)).await??; + let info = Arc::clone(&info); + let input = unblock(move || get_input(©, info)).await??; result.insert(item.placeholder.clone(), input); } Ok(result) From a0ec9caf15b68fb1e60a498d1d4767e16f2a895d Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Sat, 13 Jan 2024 20:09:43 +0900 Subject: [PATCH 02/11] + Sample module for testing --- src/migrations/create.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 6c8d6023c..33c8a42e2 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -1,3 +1,40 @@ +// module default { +// type SharesAName { +// # Change both to another scalar type, should return _ and tell user to choose between . and @ +// multi link some_link: SharesAName { +// shares_a_name: int64; +// } +// shares_a_name: int64; +// } + +// type DoesNotShareName { +// multi link other_link: DoesNotShareName { +// # Change this to another scalar type, should suggest cast with @ +// does_not_share_name: int64; +// } +// } + +// type TimeType { +// # Change to datetime, should suggest to_datetime function +// int_to_datetime: int64; +// } + +// type StrToArrayStr { +// # Change to array, should suggest a number of functions including +// # the user-defined one below +// s: array; +// } + +// function str_to_array_str(input: str) -> array using ( +// [input] +// ) + +// type ArrayToInt16 { +// # Todo: Change to int64, should recognize as array and suggest count() +// a: array; +// } +// } + use std::{ borrow::Cow, collections::{HashMap, BTreeMap}, From edf34422659abe714eb97da4311caa39c0a80743 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Sat, 13 Jan 2024 20:17:59 +0900 Subject: [PATCH 03/11] Test system time --- src/migrations/create.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 33c8a42e2..814f1efab 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -6,6 +6,7 @@ // } // shares_a_name: int64; // } +// // type DoesNotShareName { // multi link other_link: DoesNotShareName { From 3f5bb15616e0342b93349c7dc3130f4bc342e966 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Wed, 17 Jan 2024 12:52:43 +0900 Subject: [PATCH 04/11] again --- src/migrations/create.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 814f1efab..f1e127755 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -8,6 +8,7 @@ // } // +// // type DoesNotShareName { // multi link other_link: DoesNotShareName { // # Change this to another scalar type, should suggest cast with @ From d9467c198c69c6d1604fc6d4365fc99b97ec80b7 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Thu, 18 Jan 2024 11:10:49 +0900 Subject: [PATCH 05/11] Check for functions with anytype etc. in them --- src/migrations/create.rs | 95 ++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index f1e127755..4a4d99eee 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -533,24 +533,31 @@ pub fn make_default_expression(input: &RequiredUserInput) Some(expr) } -pub fn make_default_expression_interactive(input: &RequiredUserInput, info: Arc) -> Option -{ +pub fn make_default_expression_interactive( + input: &RequiredUserInput, + info: Arc, +) -> Option { let name = &input.placeholder[..]; let kind_end = name.find("_expr").unwrap_or(name.len()); - println!("Function info: {:#?}", info.function_info); let expr = match &name[..kind_end] { "fill" if input.type_name.is_some() => { - format!("<{}>{{}}", - input.type_name.as_ref().unwrap()) + format!("<{}>{{}}", input.type_name.as_ref().unwrap()) } // Please specify a conversion expression to alter the type of property 'd': // cast_expr> .d "cast" if input.pointer_name.is_some() && input.new_type.is_some() => { let pointer_name = input.pointer_name.as_deref().unwrap(); // Sometimes types will show up eg. as array for some reason instead of array - let old_type = input.old_type.as_deref().unwrap_or_default().replace('|', "::"); - let new_type = input.new_type.as_deref().unwrap_or_default().replace('|', "::"); - println!("Old type: {old_type} New type: {new_type}"); + let old_type = input + .old_type + .as_deref() + .unwrap_or_default() + .replace('|', "::"); + let new_type = input + .new_type + .as_deref() + .unwrap_or_default() + .replace('|', "::"); match (input.old_type_is_object, input.new_type_is_object) { (Some(true), Some(true)) => { format!(".{pointer_name}[IS {new_type}]") @@ -560,47 +567,66 @@ pub fn make_default_expression_interactive(input: &RequiredUserInput, info: Arc< if let Some(vals) = info.cast_info.get(&old_type) { // Then see if any of the available casts match the new type if vals.iter().any(|t| t == &new_type) { - - // cast_expr> @strength - if info.properties.link_properties.iter().any(|l| *l == pointer_name) { - if info.properties.regular_properties.iter().filter(|l| *l == pointer_name).count() > 1 { + // cast_expr> @strength + if info + .properties + .link_properties + .iter() + .any(|l| *l == pointer_name) + { + if info + .properties + .regular_properties + .iter() + .filter(|l| *l == pointer_name) + .count() + > 1 + { println!("Note: `{pointer_name}` is the name of both a regular property and a link property."); println!("Change the _ below to . if the cast is for a regular property, or to @ if the cast is for a link property.\n"); - format!("<{new_type}>_{pointer_name}") + return Some(format!("<{new_type}>_{pointer_name}")); } else { // Definitely just a link property - format!("<{new_type}>@{pointer_name}") + return Some(format!("<{new_type}>@{pointer_name}")); } } else { - format!("<{new_type}>.{pointer_name}") + return Some(format!("<{new_type}>.{pointer_name}")); } - } else { - let available_functions = info.function_info.iter().filter(|c| { - // How to check for anytype, etc.? e.g. suggest len() for any array when changing to int64 - c.input.contains(&old_type.to_string()) && c.returns.contains(&new_type) + } + } + // No casts. Now try to print out any matching functions + let available_functions = info.function_info.iter().filter(|func| { + // First see if old and new types outright match + (func.input.contains(&old_type.to_string()) && func.returns.contains(&new_type)) || + // Then see if old type is anytype and return matches + (func.input.contains("anytype") && func.returns.contains(&new_type)) || + // Then see if old type is an array of anything and return matches + (func.input.contains("array") && func.returns.contains(&new_type)) || + // Finally, see if function takes an anyreal and new type is any of its extending types + (func.input.contains("anyreal") && func.returns.contains("anyreal") && [ + "int16", "int32", "int64", "float32", "float64", "decimal"].iter().any(|e| func.returns.contains(e)) + ) }).collect::>(); - if !available_functions.is_empty() { - println!("Available functions: {available_functions:#?}"); - println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); - for function in available_functions { - let FunctionInfo { name, input, returns } = function; - println!("{name}({input}) -> {returns}"); - } - } - format!(".{pointer_name}") - } - - } else { + if !available_functions.is_empty() { + println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); + for function in available_functions { + let FunctionInfo { + name, + input, + returns, + } = function; + println!(" {name}({input}) -> {returns}"); + } + } + // Then return the pointer (maybe with matching functions, maybe not) format!(".{pointer_name}") } - } // TODO(tailhook) maybe create something for mixed case? _ => return None, } } "conv" if input.pointer_name.is_some() => { - format!("(SELECT .{} LIMIT 1)", - input.pointer_name.as_ref().unwrap()) + format!("(SELECT .{} LIMIT 1)", input.pointer_name.as_ref().unwrap()) } _ => { return None; @@ -609,6 +635,7 @@ pub fn make_default_expression_interactive(input: &RequiredUserInput, info: Arc< Some(expr) } + pub async fn unsafe_populate(_ctx: &Context, cli: &mut Connection) -> anyhow::Result { loop { From 0197cf9f3085913eb13e301fbb5c0a34227f7ad4 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Tue, 23 Jan 2024 13:10:27 +0900 Subject: [PATCH 06/11] Readability, wording, change if let to match --- src/migrations/create.rs | 125 ++++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 4a4d99eee..33d6fd86b 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -52,8 +52,8 @@ pub struct InteractiveMigrationInfo { #[derive(Queryable, Debug, Clone, Default)] pub struct PropertyInfo { - regular_properties: Vec, - link_properties: Vec + all_properties: Vec, + link_properties: Vec, } #[derive(Queryable, Debug, Clone)] @@ -73,7 +73,10 @@ async fn get_function_info(cli: &mut Connection) -> Result, Er name, input := array_join(array_agg((.params.type.name)), ''), returns := .return_type.name - };"#, &()).await + };"#, + &(), + ) + .await } #[derive(Queryable, Debug, Clone)] @@ -83,20 +86,29 @@ pub struct CastInfo { } async fn get_cast_info(cli: &mut Connection) -> Result>, Error> { - let cast_info: Vec = cli.query( - "select schema::Cast { + let cast_info: Vec = cli + .query( + "select schema::Cast { from_type_name := .from_type.name, to_type_name := .to_type.name - };", &()).await?; + };", + &(), + ) + .await?; let mut map = std::collections::HashMap::new(); for cast in cast_info { - map.entry(cast.from_type_name).or_insert(Vec::new()).push(cast.to_type_name); + map.entry(cast.from_type_name) + .or_insert(Vec::new()) + .push(cast.to_type_name); } Ok(map) } -// If regular_properties has the same link name at least twice and the name matches one inside +// If all_properties has the same link name at least twice and the name matches one inside // link_properties, then there is both a regular and a link property of the same name +// Possible todo: get info on source too. e.g. if shares_a_name is both an object property and link +// property in a whole bunch of types (but only once per type), don't want the CLI to inform the +// user about the same name async fn get_all_property_info(cli: &mut Connection) -> Result { cli.query_required_single("with @@ -106,7 +118,7 @@ async fn get_all_property_info(cli: &mut Connection) -> Result InteractiveMigrationInfo { log::debug!("Failed to find property info: {e}"); Default::default() }); - + InteractiveMigrationInfo { cast_info, function_info, @@ -192,7 +204,7 @@ enum Choice { /// Example: -/// +/// /// "required_user_input": [ /// { /// "prompt": "Please specify a conversion expression to alter the type of property 'strength'", @@ -223,8 +235,8 @@ pub struct StatementProposal { pub text: String, } -// Example: -// +// Example: +// // "proposed": { // "prompt": "did you alter the type of property 'strength' of link 'times'?", // "data_safe": false, @@ -464,7 +476,7 @@ pub async fn execute_start_migration(ctx: &Context, cli: &mut Connection) } pub async fn first_migration(cli: &mut Connection, ctx: &Context, - options: &CreateMigration) + options: &CreateMigration) -> anyhow::Result { execute_start_migration(&ctx, cli).await?; @@ -543,6 +555,9 @@ pub fn make_default_expression_interactive( "fill" if input.type_name.is_some() => { format!("<{}>{{}}", input.type_name.as_ref().unwrap()) } + "conv" if input.pointer_name.is_some() => { + format!("(SELECT .{} LIMIT 1)", input.pointer_name.as_ref().unwrap()) + } // Please specify a conversion expression to alter the type of property 'd': // cast_expr> .d "cast" if input.pointer_name.is_some() && input.new_type.is_some() => { @@ -564,10 +579,10 @@ pub fn make_default_expression_interactive( } (Some(false), Some(false)) | (None, None) => { // Check if old type has any casts - if let Some(vals) = info.cast_info.get(&old_type) { - // Then see if any of the available casts match the new type - if vals.iter().any(|t| t == &new_type) { - // cast_expr> @strength + match info.cast_info.get(&old_type) { + // and new types match the cast + Some(vals) if vals.iter().any(|t| t == &new_type) => { + // If so, then check if any link and regular properties share a name if info .properties .link_properties @@ -576,58 +591,58 @@ pub fn make_default_expression_interactive( { if info .properties - .regular_properties + .all_properties .iter() .filter(|l| *l == pointer_name) .count() > 1 { - println!("Note: `{pointer_name}` is the name of both a regular property and a link property."); - println!("Change the _ below to . if the cast is for a regular property, or to @ if the cast is for a link property.\n"); - return Some(format!("<{new_type}>_{pointer_name}")); + println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); + println!("If this object has both, then:\n <{new_type}>.{pointer_name} will cast from the object type property, while\n <{new_type}>@{pointer_name} will cast from the link property."); + format!("<{new_type}>_{pointer_name}") } else { // Definitely just a link property - return Some(format!("<{new_type}>@{pointer_name}")); + format!("<{new_type}>@{pointer_name}") } } else { - return Some(format!("<{new_type}>.{pointer_name}")); + // No link properties of the same name found + format!("<{new_type}>.{pointer_name}") } } - } - // No casts. Now try to print out any matching functions - let available_functions = info.function_info.iter().filter(|func| { - // First see if old and new types outright match - (func.input.contains(&old_type.to_string()) && func.returns.contains(&new_type)) || - // Then see if old type is anytype and return matches - (func.input.contains("anytype") && func.returns.contains(&new_type)) || - // Then see if old type is an array of anything and return matches - (func.input.contains("array") && func.returns.contains(&new_type)) || - // Finally, see if function takes an anyreal and new type is any of its extending types - (func.input.contains("anyreal") && func.returns.contains("anyreal") && [ - "int16", "int32", "int64", "float32", "float64", "decimal"].iter().any(|e| func.returns.contains(e)) - ) - }).collect::>(); - if !available_functions.is_empty() { - println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); - for function in available_functions { - let FunctionInfo { - name, - input, - returns, - } = function; - println!(" {name}({input}) -> {returns}"); + _ => { + // No matching casts between old and new type. Now try to print out any matching functions + let available_functions = info.function_info.iter().filter(|func| { + // First see if old and new types outright match + (func.input.contains(&old_type.to_string()) && func.returns.contains(&new_type)) || + // Then see if old type is anytype and return matches + (func.input.contains("anytype") && func.returns.contains(&new_type)) || + // Then see if old type is an array of anything and return matches + (func.input.contains("array") && func.returns.contains(&new_type)) || + // Finally, see if function takes an anyreal and new type is any of its extending types + (func.input.contains("anyreal") && func.returns.contains("anyreal") && [ + "int16", "int32", "int64", "float32", "float64", "decimal"].iter().any(|e| func.returns.contains(e)) + ) + }).collect::>(); + if !available_functions.is_empty() { + println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); + for function in available_functions { + let FunctionInfo { + name, + input, + returns, + } = function; + println!(" {name}({input}) -> {returns}"); + } + } + // Then return the pointer (maybe with matching functions, maybe not) + format!(".{pointer_name}") } } - // Then return the pointer (maybe with matching functions, maybe not) - format!(".{pointer_name}") } // TODO(tailhook) maybe create something for mixed case? _ => return None, } } - "conv" if input.pointer_name.is_some() => { - format!("(SELECT .{} LIMIT 1)", input.pointer_name.as_ref().unwrap()) - } _ => { return None; } @@ -790,7 +805,7 @@ impl<'a> InteractiveMigration<'a> { } async fn run(mut self, info: Arc) -> anyhow::Result { self.save_point().await?; - loop { + loop { let descr = query_row::(self.cli, "DESCRIBE CURRENT MIGRATION AS JSON", ).await?; @@ -958,7 +973,7 @@ async fn run_interactive(_ctx: &Context, cli: &mut Connection, key: MigrationKey, options: &CreateMigration, info: Arc) -> anyhow::Result { - + let descr = InteractiveMigration::new(cli).await?.run(info).await?; if descr.confirmed.is_empty() && !options.allow_empty { @@ -1087,7 +1102,7 @@ pub async fn normal_migration(cli: &mut Connection, ctx: &Context, create: &CreateMigration) -> anyhow::Result { - let info = Arc::new(get_migration_info(cli).await); +let info = Arc::new(get_migration_info(cli).await); execute_start_migration(&ctx, cli).await?; async_try! { async { From 490ec1b2f0a2c3a4f33bce5f78f86017d7cfcf6e Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Tue, 23 Jan 2024 21:07:37 +0900 Subject: [PATCH 07/11] Improve query for properties --- src/migrations/create.rs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 33d6fd86b..01386d942 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -52,7 +52,7 @@ pub struct InteractiveMigrationInfo { #[derive(Queryable, Debug, Clone, Default)] pub struct PropertyInfo { - all_properties: Vec, + regular_properties: Vec, link_properties: Vec, } @@ -110,15 +110,11 @@ async fn get_cast_info(cli: &mut Connection) -> Result Result { - cli.query_required_single("with - - links := (select schema::Link filter .builtin = false), - link_names := (select names := links.properties.name filter names not in {'__type__', 'target', 'source'}), - - properties := (select schema::Property filter .builtin = false), - property_names := (select names := properties.name filter names not in {'id', 'target', 'source'}), - - select {all_properties := property_names, link_properties := distinct link_names, };", &()).await + cli.query_required_single("with + all_props := (select schema::Property filter .builtin = false), + props := (select all_props filter .source is schema::ObjectType and .name != 'id'), + links := (select all_props filter .source is schema::Link and .name not in {'target', 'source'}), + select { regular_properties := props.name, link_properties := links.name };", &()).await } // Don't want to fail CLI if migration info can't be found, just log and return default @@ -591,11 +587,9 @@ pub fn make_default_expression_interactive( { if info .properties - .all_properties + .regular_properties .iter() - .filter(|l| *l == pointer_name) - .count() - > 1 + .any(|l| *l == pointer_name) { println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); println!("If this object has both, then:\n <{new_type}>.{pointer_name} will cast from the object type property, while\n <{new_type}>@{pointer_name} will cast from the link property."); From 14dd14469bf8aedaae5b9463479b8ad84914ffe9 Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Tue, 23 Jan 2024 21:25:33 +0900 Subject: [PATCH 08/11] Clean up, move around --- src/migrations/create.rs | 256 ++++++++++++++++----------------------- 1 file changed, 105 insertions(+), 151 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 01386d942..0be52566e 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -1,145 +1,9 @@ -// module default { -// type SharesAName { -// # Change both to another scalar type, should return _ and tell user to choose between . and @ -// multi link some_link: SharesAName { -// shares_a_name: int64; -// } -// shares_a_name: int64; -// } -// - -// -// type DoesNotShareName { -// multi link other_link: DoesNotShareName { -// # Change this to another scalar type, should suggest cast with @ -// does_not_share_name: int64; -// } -// } - -// type TimeType { -// # Change to datetime, should suggest to_datetime function -// int_to_datetime: int64; -// } - -// type StrToArrayStr { -// # Change to array, should suggest a number of functions including -// # the user-defined one below -// s: array; -// } - -// function str_to_array_str(input: str) -> array using ( -// [input] -// ) - -// type ArrayToInt16 { -// # Todo: Change to int64, should recognize as array and suggest count() -// a: array; -// } -// } - use std::{ borrow::Cow, collections::{HashMap, BTreeMap}, path::{Path, PathBuf}, sync::Arc }; -#[derive(Clone, Debug, Default)] -pub struct InteractiveMigrationInfo { - cast_info: HashMap>, - function_info: Vec, - properties: PropertyInfo -} - -#[derive(Queryable, Debug, Clone, Default)] -pub struct PropertyInfo { - regular_properties: Vec, - link_properties: Vec, -} - -#[derive(Queryable, Debug, Clone)] -pub struct FunctionInfo { - name: String, - input: String, - returns: String -} - -// Returns all functions as long as the type returned input type does not equal output -// (Casts only required when changing to a new type) -// Currently returns 161 functions -async fn get_function_info(cli: &mut Connection) -> Result, Error> { - cli.query::( - r#"with fn := (select schema::Function filter count(.params.type.name) = 1), - select fn { - name, - input := array_join(array_agg((.params.type.name)), ''), - returns := .return_type.name - };"#, - &(), - ) - .await -} - -#[derive(Queryable, Debug, Clone)] -pub struct CastInfo { - from_type_name: String, - to_type_name: String -} - -async fn get_cast_info(cli: &mut Connection) -> Result>, Error> { - let cast_info: Vec = cli - .query( - "select schema::Cast { - from_type_name := .from_type.name, - to_type_name := .to_type.name - };", - &(), - ) - .await?; - let mut map = std::collections::HashMap::new(); - for cast in cast_info { - map.entry(cast.from_type_name) - .or_insert(Vec::new()) - .push(cast.to_type_name); - } - Ok(map) -} - -// If all_properties has the same link name at least twice and the name matches one inside -// link_properties, then there is both a regular and a link property of the same name -// Possible todo: get info on source too. e.g. if shares_a_name is both an object property and link -// property in a whole bunch of types (but only once per type), don't want the CLI to inform the -// user about the same name -async fn get_all_property_info(cli: &mut Connection) -> Result { - cli.query_required_single("with - all_props := (select schema::Property filter .builtin = false), - props := (select all_props filter .source is schema::ObjectType and .name != 'id'), - links := (select all_props filter .source is schema::Link and .name not in {'target', 'source'}), - select { regular_properties := props.name, link_properties := links.name };", &()).await -} - -// Don't want to fail CLI if migration info can't be found, just log and return default -async fn get_migration_info(cli: &mut Connection) -> InteractiveMigrationInfo { - let function_info = get_function_info(cli).await.unwrap_or_else(|e| { - println!("Failed to find function info: {e}"); - log::debug!("Failed to find function info: {e}"); - Default::default() - }); - let cast_info = get_cast_info(cli).await.unwrap_or_else(|e| { - log::debug!("Failed to find cast_info: {e}"); - Default::default() - }); - let properties = get_all_property_info(cli).await.unwrap_or_else(|e| { - log::debug!("Failed to find property info: {e}"); - Default::default() - }); - - InteractiveMigrationInfo { - cast_info, - function_info, - properties - } -} - use anyhow::Context as _; use colorful::Colorful; use edgedb_derive::Queryable; @@ -199,19 +63,19 @@ enum Choice { } -/// Example: -/// -/// "required_user_input": [ -/// { -/// "prompt": "Please specify a conversion expression to alter the type of property 'strength'", -/// "new_type": "std::str", -/// "old_type": "std::int64", -/// "placeholder": "cast_expr", -/// "pointer_name": "strength", -/// "new_type_is_object": false, -/// "old_type_is_object": false -/// } -/// ] +// Example: +// +// "required_user_input": [ +// { +// "prompt": "Please specify a conversion expression to alter the type of property 'strength'", +// "new_type": "std::str", +// "old_type": "std::int64", +// "placeholder": "cast_expr", +// "pointer_name": "strength", +// "new_type_is_object": false, +// "old_type_is_object": false +// } +// ] #[derive(Deserialize, Debug, Clone)] pub struct RequiredUserInput { placeholder: String, @@ -233,7 +97,7 @@ pub struct StatementProposal { // Example: // -// "proposed": { +// "proposed": { // "prompt": "did you alter the type of property 'strength' of link 'times'?", // "data_safe": false, // "prompt_id": "SetPropertyType PROPERTY default::__|strength@default|__||times&default||Time", @@ -254,7 +118,7 @@ pub struct Proposal { } // Returned from each DESCRIBE CURRENT SCHEMA AS JSON during a migration. -// e.g. +// Example: // // { // "parent": "m17emwiottbbfc4coo7ybcrkljr5bhdv46ouoyyjrsj4qwvg7w5ina", @@ -321,6 +185,96 @@ struct InteractiveMigration<'a> { confirmed: Vec, } +#[derive(Clone, Debug, Default)] +pub struct InteractiveMigrationInfo { + cast_info: HashMap>, + function_info: Vec, + properties: PropertyInfo +} + +#[derive(Queryable, Debug, Clone, Default)] +pub struct PropertyInfo { + regular_properties: Vec, + link_properties: Vec, +} + +#[derive(Queryable, Debug, Clone)] +pub struct FunctionInfo { + name: String, + input: String, + returns: String +} + +// Returns all functions as long as the type returned input type does not equal output +// (Casts only required when changing to a new type) +async fn get_function_info(cli: &mut Connection) -> Result, Error> { + cli.query::( + r#"with fn := (select schema::Function filter count(.params.type.name) = 1), + select fn { + name, + input := array_join(array_agg((.params.type.name)), ''), + returns := .return_type.name + };"#, + &(), + ) + .await +} + +#[derive(Queryable, Debug, Clone)] +pub struct CastInfo { + from_type_name: String, + to_type_name: String +} + +async fn get_cast_info(cli: &mut Connection) -> Result>, Error> { + let cast_info: Vec = cli + .query( + "select schema::Cast { + from_type_name := .from_type.name, + to_type_name := .to_type.name + };", + &(), + ) + .await?; + let mut map = std::collections::HashMap::new(); + for cast in cast_info { + map.entry(cast.from_type_name) + .or_insert(Vec::new()) + .push(cast.to_type_name); + } + Ok(map) +} + +async fn get_all_property_info(cli: &mut Connection) -> Result { + cli.query_required_single("with + all_props := (select schema::Property filter .builtin = false), + props := (select all_props filter .source is schema::ObjectType and .name != 'id'), + links := (select all_props filter .source is schema::Link and .name not in {'target', 'source'}), + select { regular_properties := props.name, link_properties := links.name };", &()).await +} + +// Don't want to fail CLI if migration info can't be found, just log and return default +async fn get_migration_info(cli: &mut Connection) -> InteractiveMigrationInfo { + let function_info = get_function_info(cli).await.unwrap_or_else(|e| { + log::debug!("Failed to find function info: {e}"); + Default::default() + }); + let cast_info = get_cast_info(cli).await.unwrap_or_else(|e| { + log::debug!("Failed to find cast_info: {e}"); + Default::default() + }); + let properties = get_all_property_info(cli).await.unwrap_or_else(|e| { + log::debug!("Failed to find property info: {e}"); + Default::default() + }); + + InteractiveMigrationInfo { + cast_info, + function_info, + properties + } +} + #[derive(Debug, thiserror::Error)] #[error("refused to input data required for placeholder")] struct Refused; From 49f8a519cdc0df059bce65cf3e8ecf8c9f07acdd Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Tue, 23 Jan 2024 22:12:14 +0900 Subject: [PATCH 09/11] Enum to avoid duplication --- src/migrations/create.rs | 64 +++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 0be52566e..81458f6d5 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -198,6 +198,32 @@ pub struct PropertyInfo { link_properties: Vec, } +enum PropertyKind { + RegularProperty, + LinkProperty, + BothProperties +} + +impl PropertyInfo { + fn property_check(&self, pointer_name: &str) -> PropertyKind { + if self + .link_properties + .iter() + .any(|l| *l == pointer_name) { + if self + .regular_properties + .iter() + .any(|l| *l == pointer_name) { + PropertyKind::BothProperties + } else { + PropertyKind::LinkProperty + } + } else { + PropertyKind::RegularProperty + } + } +} + #[derive(Queryable, Debug, Clone)] pub struct FunctionInfo { name: String, @@ -533,28 +559,18 @@ pub fn make_default_expression_interactive( // and new types match the cast Some(vals) if vals.iter().any(|t| t == &new_type) => { // If so, then check if any link and regular properties share a name - if info - .properties - .link_properties - .iter() - .any(|l| *l == pointer_name) - { - if info - .properties - .regular_properties - .iter() - .any(|l| *l == pointer_name) - { + match info.properties.property_check(pointer_name) { + PropertyKind::BothProperties => { println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); println!("If this object has both, then:\n <{new_type}>.{pointer_name} will cast from the object type property, while\n <{new_type}>@{pointer_name} will cast from the link property."); format!("<{new_type}>_{pointer_name}") - } else { - // Definitely just a link property + } + PropertyKind::LinkProperty => { format!("<{new_type}>@{pointer_name}") } - } else { - // No link properties of the same name found - format!("<{new_type}>.{pointer_name}") + PropertyKind::RegularProperty => { + format!("<{new_type}>.{pointer_name}") + } } } _ => { @@ -583,7 +599,19 @@ pub fn make_default_expression_interactive( } } // Then return the pointer (maybe with matching functions, maybe not) - format!(".{pointer_name}") + match info.properties.property_check(pointer_name) { + PropertyKind::BothProperties => { + println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); + println!("If this object has both, then:\n .{pointer_name} will access the object type property, while\n @{pointer_name} will access the link property."); + format!("{pointer_name}") + } + PropertyKind::LinkProperty => { + format!("@{pointer_name}") + } + PropertyKind::RegularProperty => { + format!(".{pointer_name}") + } + } } } } From ef9aabfc57b650ad6d46b6bd663538b9883e185b Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Thu, 8 Feb 2024 11:58:31 +0900 Subject: [PATCH 10/11] rv check for anytype functions --- src/migrations/create.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index 81458f6d5..aa9a1579e 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -577,11 +577,9 @@ pub fn make_default_expression_interactive( // No matching casts between old and new type. Now try to print out any matching functions let available_functions = info.function_info.iter().filter(|func| { // First see if old and new types outright match - (func.input.contains(&old_type.to_string()) && func.returns.contains(&new_type)) || - // Then see if old type is anytype and return matches - (func.input.contains("anytype") && func.returns.contains(&new_type)) || + (func.input == old_type && func.returns == new_type) || // Then see if old type is an array of anything and return matches - (func.input.contains("array") && func.returns.contains(&new_type)) || + (func.input.contains("array") && func.returns == new_type) || // Finally, see if function takes an anyreal and new type is any of its extending types (func.input.contains("anyreal") && func.returns.contains("anyreal") && [ "int16", "int32", "int64", "float32", "float64", "decimal"].iter().any(|e| func.returns.contains(e)) From 00f96aa54ae9b524f2aa48c8784e7319fd9009de Mon Sep 17 00:00:00 2001 From: Dhghomon Date: Thu, 8 Feb 2024 23:09:12 +0900 Subject: [PATCH 11/11] Only check for exact input/return type equality, change output a bit --- src/migrations/create.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/migrations/create.rs b/src/migrations/create.rs index c093665f5..4443ad963 100644 --- a/src/migrations/create.rs +++ b/src/migrations/create.rs @@ -566,9 +566,8 @@ pub fn make_default_expression_interactive( // If so, then check if any link and regular properties share a name match info.properties.property_check(pointer_name) { PropertyKind::BothProperties => { - println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); - println!("If this object has both, then:\n <{new_type}>.{pointer_name} will cast from the object type property, while\n <{new_type}>@{pointer_name} will cast from the link property."); - format!("<{new_type}>_{pointer_name}") + println!(" Note: Change .{pointer_name} to @{pointer_name} if `{pointer_name}` refers to a link property."); + format!("<{new_type}>.{pointer_name}") } PropertyKind::LinkProperty => { format!("<{new_type}>@{pointer_name}") @@ -581,17 +580,10 @@ pub fn make_default_expression_interactive( _ => { // No matching casts between old and new type. Now try to print out any matching functions let available_functions = info.function_info.iter().filter(|func| { - // First see if old and new types outright match - (func.input == old_type && func.returns == new_type) || - // Then see if old type is an array of anything and return matches - (func.input.contains("array") && func.returns == new_type) || - // Finally, see if function takes an anyreal and new type is any of its extending types - (func.input.contains("anyreal") && func.returns.contains("anyreal") && [ - "int16", "int32", "int64", "float32", "float64", "decimal"].iter().any(|e| func.returns.contains(e)) - ) + func.input == old_type && func.returns == new_type }).collect::>(); if !available_functions.is_empty() { - println!("Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); + println!(" Note: The following function{plural} may help you convert from {old_type} to {new_type}:", plural = if available_functions.len() > 2 {"s"} else {""}); for function in available_functions { let FunctionInfo { name, @@ -604,9 +596,8 @@ pub fn make_default_expression_interactive( // Then return the pointer (maybe with matching functions, maybe not) match info.properties.property_check(pointer_name) { PropertyKind::BothProperties => { - println!("Note: Your schema has both object and link properties with the name `{pointer_name}`."); - println!("If this object has both, then:\n .{pointer_name} will access the object type property, while\n @{pointer_name} will access the link property."); - format!("{pointer_name}") + println!(" Note: Change .{pointer_name} to @{pointer_name} if `{pointer_name}` refers to a link property."); + format!(".{pointer_name}") } PropertyKind::LinkProperty => { format!("@{pointer_name}")