Repository: prql/prql (rust)

Issue:
Escape single quote characters in strings
Following up to https://github.com/prql/prql/pull/439, Currently

```elm
derive x: "two households'"
```

compiles to:

```sql
SELECT
  ' two households ' '' AS x
```


Whereas I think it should compile to:


```sql
SELECT
  'two households\'' AS x
```

I'm actually not sure why it has spaces around it, and where `' ''` is coming from.

Pull request:
Fix string escapes, refactor
Closes #440

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index edfe6fc0..77ef4ec9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1801,9 +1801,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
 
 [[package]]
 name = "sqlformat"
-version = "0.1.8"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4"
+checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a"
 dependencies = [
  "itertools",
  "nom",
diff --git a/prql-compiler/Cargo.toml b/prql-compiler/Cargo.toml
index 8466ac0c..aed5e689 100644
--- a/prql-compiler/Cargo.toml
+++ b/prql-compiler/Cargo.toml
@@ -27,7 +27,7 @@ pest = "2.1.3"
 pest_derive = "2.1.0"
 serde_json = "1.0.79"
 serde_yaml = "0.8.23"
-sqlformat = "0.1.8"
+sqlformat = "0.2.0"
 strum = {version = "0.24.0", features = ["std", "derive"]}# for converting enum variants to string
 strum_macros = "0.24.0"
 
diff --git a/prql-compiler/src/ast/ast_fold.rs b/prql-compiler/src/ast/ast_fold.rs
index c63176b1..a7b1aa55 100644
--- a/prql-compiler/src/ast/ast_fold.rs
+++ b/prql-compiler/src/ast/ast_fold.rs
@@ -126,7 +126,8 @@ pub fn fold_item<T: ?Sized + AstFold>(fold: &mut T, item: Item) -> Result<Item>
         | Item::Interval(_)
         | Item::Date(_)
         | Item::Time(_)
-        | Item::Timestamp(_) => item,
+        | Item::Timestamp(_)
+        | Item::Boolean(_) => item,
     })
 }
 
diff --git a/prql-compiler/src/ast/item.rs b/prql-compiler/src/ast/item.rs
index 07ca6197..39069289 100644
--- a/prql-compiler/src/ast/item.rs
+++ b/prql-compiler/src/ast/item.rs
@@ -33,6 +33,7 @@ pub enum Item {
     Date(String),
     Time(String),
     Timestamp(String),
+    Boolean(bool),
     Windowed(Windowed),
 }
 
@@ -255,6 +256,9 @@ impl Display for Item {
             Item::Date(inner) | Item::Time(inner) | Item::Timestamp(inner) => {
                 write!(f, "@{inner}")?;
             }
+            Item::Boolean(b) => {
+                f.write_str(if *b { "true" } else { "false" })?;
+            }
             Item::Windowed(w) => {
                 write!(f, "{:?}", w.expr)?;
             }
diff --git a/prql-compiler/src/parser.rs b/prql-compiler/src/parser.rs
index 3aa94232..729d3094 100644
--- a/prql-compiler/src/parser.rs
+++ b/prql-compiler/src/parser.rs
@@ -169,11 +169,12 @@ fn ast_of_parse_tree(pairs: Pairs<Rule>) -> Result<Vec<Node>> {
                     })
                 }
                 Rule::ident => Item::Ident(pair.as_str().to_string()),
-                Rule::string => Item::String(
-                    // Put the string_inner (which doesn't have quotes) into the
-                    // String item.
-                    pair.into_inner().into_only()?.as_str().to_string(),
-                ),
+                Rule::string => {
+                    let inner = pair.into_inner().into_only()?.as_str().to_string();
+
+                    // Put string_inner (which doesn't have quotes) into Item::String.
+                    Item::String(inner)
+                }
                 Rule::s_string => Item::SString(ast_of_interpolate_items(pair)?),
                 Rule::f_string => Item::FString(ast_of_interpolate_items(pair)?),
                 Rule::pipeline => Item::Pipeline(Pipeline {
@@ -250,6 +251,7 @@ fn ast_of_parse_tree(pairs: Pairs<Rule>) -> Result<Vec<Node>> {
 
                     Item::Type(Type { name, param })
                 }
+                Rule::boolean => Item::Boolean(pair.as_str() == "true"),
                 Rule::number
                 | Rule::interval_kind
                 | Rule::date_inner
diff --git a/prql-compiler/src/prql.pest b/prql-compiler/src/prql.pest
index 91312163..9b248241 100644
--- a/prql-compiler/src/prql.pest
+++ b/prql-compiler/src/prql.pest
@@ -71,8 +71,8 @@ expr_compare = !{ expr_add ~ (operator_compare ~ expr_add)? }
 expr_add = !{ expr_mul ~ (operator_add ~ expr_mul)* }
 expr_mul = !{ term ~ (operator_mul ~ term)* }
 
-term = _{ ( s_string | f_string | ident | nested_expr | nested_pipeline | list | range | literal ) }
-literal = _{ interval | number | string | timestamp | date | time }
+term = _{ ( s_string | f_string | range | literal | ident | nested_expr | nested_pipeline | list ) }
+literal = _{ interval | number | boolean | string | timestamp | date | time }
 list = { "[" ~ (NEWLINE* ~ (assign_call | expr_call) ~ ("," ~ NEWLINE* ~ (assign_call | expr_call) )* ~ ","?)? ~ NEWLINE* ~ "]" }
 nested_expr = _{ "(" ~ expr_call ~ ")" }
 nested_pipeline = { "(" ~ expr_call? ~ pipe ~ pipeline ~ NEWLINE* ~ ")" }
@@ -101,6 +101,8 @@ string = ${ ( PUSH(multi_quote) | PUSH(quote) ) ~ string_inner ~ POP }
 
 number = ${ ( ASCII_DIGIT )+ ~ ("." ~ ( ASCII_DIGIT )+)? }
 
+boolean = ${ "true" | "false" }
+
 range = ${ range_edge ~ ".." ~ range_edge }
 // We need `literal` separate from `term_simple` for things like range edges,
 // which would infinitely recurse otherwise, since it'll keep trying to parse
diff --git a/prql-compiler/src/sql/translator.rs b/prql-compiler/src/sql/translator.rs
index 8780ea1a..5679bb85 100644
--- a/prql-compiler/src/sql/translator.rs
+++ b/prql-compiler/src/sql/translator.rs
@@ -12,11 +12,10 @@ use anyhow::{anyhow, bail, Result};
 use itertools::Itertools;
 use sqlformat::{format, FormatOptions, QueryParams};
 use sqlparser::ast::{
-    self as sql_ast, Expr, Function, FunctionArg, FunctionArgExpr, Join, JoinConstraint,
-    JoinOperator, ObjectName, OrderByExpr, Select, SelectItem, SetExpr, TableAlias, TableFactor,
-    TableWithJoins, Top, WindowSpec,
+    self as sql_ast, DateTimeField, Expr, Function, FunctionArg, FunctionArgExpr, Join,
+    JoinConstraint, JoinOperator, ObjectName, OrderByExpr, Select, SelectItem, SetExpr, TableAlias,
+    TableFactor, TableWithJoins, Top, Value, WindowSpec,
 };
-use sqlparser::ast::{DateTimeField, Value};
 use std::collections::HashMap;
 
 use crate::ast::JoinFilter;
@@ -124,14 +123,6 @@ fn table_to_sql_cte(table: AtomicTable, dialect: &Dialect) -> Result<sql_ast::Ct
     })
 }
 
-// TODO: currently we can't implement a TryFrom for Pipeline because it's a type
-// alias. Possibly at some point we should turn it into a wrapper type. (We can
-// still implement Iter & IntoIterator, though.)
-//
-// impl TryFrom<Pipeline> for sqlparser::sql_ast::Query {
-//     type Error = anyhow::Error;
-//     fn try_from(&pipeline: Pipeline) -> Result<sqlparser::sql_ast::Query> {
-
 fn table_factor_of_table_ref(table_ref: &TableRef) -> TableFactor {
     TableFactor::Table {
         name: ObjectName(vec![Item::Ident(table_ref.name.clone())
@@ -482,7 +473,7 @@ impl TryFrom<Item> for Expr {
                 let end: Expr = assert_bound(r.end)?.item.try_into()?;
                 Expr::Identifier(sql_ast::Ident::new(format!("{} AND {}", start, end)))
             }
-            Item::String(s) => Expr::Value(sql_ast::Value::SingleQuotedString(s)),
+            Item::String(s) => Expr::Value(Value::SingleQuotedString(s)),
             // Fairly hacky — convert everything to a string, then concat it,
             // then convert to Expr. We can't use the `Item::Expr` code above
             // since we don't want to intersperse with spaces.
@@ -504,7 +495,7 @@ impl TryFrom<Item> for Expr {
                     .into_iter()
                     .map(|item| match item {
                         InterpolateItem::String(string) => {
-                            Ok(Expr::Value(sql_ast::Value::SingleQuotedString(string)))
+                            Ok(Expr::Value(Value::SingleQuotedString(string)))
                         }
                         InterpolateItem::Expr(node) => Expr::try_from(node.item),
                     })
@@ -561,6 +552,7 @@ impl TryFrom<Item> for Expr {
                 data_type: sql_ast::DataType::Timestamp,
                 value: literal,
             },
+            Item::Boolean(b) => Expr::Value(Value::Boolean(b)),
 
             _ => bail!("Can't convert to Expr; {item:?}"),
         })
@@ -1438,4 +1430,40 @@ take 20
 
         Ok(())
     }
+
+    #[test]
+    fn test_strings() -> Result<()> {
+        let query: Query = parse(
+            r###"
+        derive [
+            x = "two households'",
+            y = 'two households"',
+            z = f"a {x} b' {y} c",
+            v = f'a {x} b" {y} c',
+        ]
+        "###,
+        )?;
+
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          'two households''' AS x,
+          'two households"' AS y,
+          CONCAT(
+            'a ',
+            'two households''',
+            ' b'' ',
+            'two households"',
+            ' c'
+          ) AS z,
+          CONCAT(
+            'a ',
+            'two households''',
+            ' b" ',
+            'two households"',
+            ' c'
+          ) AS v
+        "###);
+
+        Ok(())
+    }
 }


Test patch:
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@introduction-0.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@introduction-0.prql.snap
index dd0fe54e..090e006f 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@introduction-0.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@introduction-0.prql.snap
@@ -33,7 +33,7 @@ SELECT
   table_0.*,
   countries.*,
   country_code,
-  countries.true AS always_true,
+  true AS always_true,
   version() AS db_version
 FROM
   table_0