Repository: prql/prql (rust)

Issue:
Add PRQL Version comment to the generated SQL
I was wondering whether it might be useful to have a comment like
```sql
-- Generated by PRQL version 0.2.11 (www.prql-lang.org)
```
at the top (or bottom) of the generated SQL?

Maybe in the Playground as well as in `prqlc` with the option to disable / enable it whatever the default ends up being?

That way when you copy and paste SQL to Github issues or dbt models or wherever, you have a reference point where it came from.

If you have Github issues that remain open for a long time, you may not be able to reproduce the output as it was posted in the issue and this would allow you to identify when the behaviour was as described in the report.

---
I also think it could serve as a form of marketing: if someone comes across such SQL code in the wild and they don't know PRQL then they may be prompted to investigate what PRQL is.

Pull request description:
feat: compiler signature
Closes #1154

Patch:
diff --git a/prql-compiler/src/lib.rs b/prql-compiler/src/lib.rs
index f0161cf0..a3019ba0 100644
--- a/prql-compiler/src/lib.rs
+++ b/prql-compiler/src/lib.rs
@@ -52,7 +52,7 @@ mod utils;
 
 #[cfg(all(feature = "cli", not(target_family = "wasm")))]
 pub use cli::Cli;
-pub use error::{ErrorMessage, ErrorMessages, SourceLocation};
+pub use error::{downcast, ErrorMessage, ErrorMessages, SourceLocation};
 pub use utils::IntoOnly;
 
 use once_cell::sync::Lazy;
diff --git a/prql-compiler/src/semantic/resolver.rs b/prql-compiler/src/semantic/resolver.rs
index 6f3a79b3..caa7d50d 100644
--- a/prql-compiler/src/semantic/resolver.rs
+++ b/prql-compiler/src/semantic/resolver.rs
@@ -626,10 +626,9 @@ mod test {
     use crate::ast::pl::{Expr, Ty};
     use crate::semantic::resolve_only;
     use crate::utils::IntoOnly;
-    use crate::{compile, parser::parse};
 
     fn parse_and_resolve(query: &str) -> Result<Expr> {
-        let (stmts, _) = resolve_only(parse(query)?, None)?;
+        let (stmts, _) = resolve_only(crate::parser::parse(query)?, None)?;
 
         Ok(*stmts.into_only()?.kind.into_pipeline()?)
     }
@@ -646,7 +645,7 @@ mod test {
 
     #[test]
     fn test_func_call_resolve() {
-        assert_display_snapshot!(compile(r#"
+        assert_display_snapshot!(crate::test::compile(r#"
         from employees
         aggregate [
           count non_null:salary,
diff --git a/prql-compiler/src/sql/mod.rs b/prql-compiler/src/sql/mod.rs
index 8bb5399e..33016fda 100644
--- a/prql-compiler/src/sql/mod.rs
+++ b/prql-compiler/src/sql/mod.rs
@@ -8,14 +8,59 @@ mod preprocess;
 mod translator;
 
 pub use dialect::Dialect;
+
+use anyhow::Result;
 use serde::{Deserialize, Serialize};
-pub use translator::compile;
+
+use crate::{ast::rq::Query, PRQL_VERSION};
+
+/// Translate a PRQL AST into a SQL string.
+pub fn compile(query: Query, options: Option<Options>) -> Result<String> {
+    let options = options.unwrap_or_default();
+
+    let sql_ast = translator::translate_query(query, options.dialect)?;
+
+    let sql = sql_ast.to_string();
+
+    // formatting
+    let sql = if options.format {
+        let formatted = sqlformat::format(
+            &sql,
+            &sqlformat::QueryParams::default(),
+            sqlformat::FormatOptions::default(),
+        );
+
+        // The sql formatter turns `{{` into `{ {`, and while that's reasonable SQL,
+        // we want to allow jinja expressions through. So we (somewhat hackily) replace
+        // any `{ {` with `{{`.
+        formatted.replace("{ {", "{{").replace("} }", "}}")
+    } else {
+        sql
+    };
+
+    // signature
+    let sql = if options.signature_comment {
+        let pre = if options.format { "\n\n" } else { " " };
+        let post = if options.format { "\n" } else { "" };
+        let signature = format!(
+            "{pre}-- Generated by PRQL compiler version {} (https://prql-lang.org){post}",
+            *PRQL_VERSION
+        );
+        sql + &signature
+    } else {
+        sql
+    };
+
+    Ok(sql)
+}
 
 /// Compilation options for SQL backend of the compiler.
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Options {
-    /// True for passing generated SQL string trough a formatter that splits
+    /// Pass generated SQL string trough a formatter that splits it
     /// into multiple lines and prettifies indentation and spacing.
+    ///
+    /// Defaults to true.
     pub format: bool,
 
     /// Target dialect you want to compile for.
@@ -30,6 +75,11 @@ pub struct Options {
     /// If None is used, `sql_dialect` flag from query definition is used.
     /// If it does not exist, [Dialect::Generic] is used.
     pub dialect: Option<Dialect>,
+
+    /// Emits the compiler signature as a comment after generated SQL
+    ///
+    /// Defaults to true.
+    pub signature_comment: bool,
 }
 
 impl Default for Options {
@@ -37,6 +87,28 @@ impl Default for Options {
         Self {
             format: true,
             dialect: None,
+            signature_comment: true,
         }
     }
 }
+
+impl Options {
+    pub fn no_format(mut self) -> Self {
+        self.format = false;
+        self
+    }
+
+    pub fn no_signature(mut self) -> Self {
+        self.signature_comment = false;
+        self
+    }
+
+    pub fn with_dialect(mut self, dialect: Dialect) -> Self {
+        self.dialect = Some(dialect);
+        self
+    }
+
+    pub fn some(self) -> Option<Self> {
+        Some(self)
+    }
+}
diff --git a/prql-compiler/src/sql/translator.rs b/prql-compiler/src/sql/translator.rs
index f297a3ce..beceda6c 100644
--- a/prql-compiler/src/sql/translator.rs
+++ b/prql-compiler/src/sql/translator.rs
@@ -7,7 +7,6 @@ use std::str::FromStr;
 
 use anyhow::{anyhow, Result};
 use itertools::Itertools;
-use sqlformat::{format, FormatOptions, QueryParams};
 use sqlparser::ast::{self as sql_ast, Select, SelectItem, SetExpr, TableWithJoins};
 
 use crate::ast::pl::{BinOp, Literal};
@@ -21,7 +20,6 @@ use crate::utils::{BreakUp, IntoOnly, Pluck, TableCounter};
 
 use super::codegen::*;
 use super::preprocess::{preprocess_distinct, preprocess_reorder};
-use super::Options;
 use super::{anchor, Dialect};
 use super::{context::AnchorContext, dialect::DialectHandler};
 
@@ -38,30 +36,6 @@ pub(super) struct Context {
     pub pre_projection: bool,
 }
 
-/// Translate a PRQL AST into a SQL string.
-pub fn compile(query: Query, options: Option<Options>) -> Result<String> {
-    let options = options.unwrap_or_default();
-
-    let sql_ast = translate_query(query, options.dialect)?;
-
-    let sql_string = sql_ast.to_string();
-
-    Ok(if options.format {
-        let formatted = format(
-            &sql_string,
-            &QueryParams::default(),
-            FormatOptions::default(),
-        );
-
-        // The sql formatter turns `{{` into `{ {`, and while that's reasonable SQL,
-        // we want to allow jinja expressions through. So we (somewhat hackily) replace
-        // any `{ {` with `{{`.
-        formatted.replace("{ {", "{{").replace("} }", "}}")
-    } else {
-        sql_string
-    })
-}
-
 pub fn translate_query(query: Query, dialect: Option<Dialect>) -> Result<sql_ast::Query> {
     let dialect = if let Some(dialect) = dialect {
         dialect
@@ -655,9 +629,7 @@ mod test {
         )
         "#;
 
-        let query = resolve(parse(query).unwrap()).unwrap();
-
-        let sql_ast = compile(query, None).unwrap();
+        let sql_ast = crate::test::compile(query).unwrap();
 
         assert_snapshot!(sql_ast);
     }
@@ -680,9 +652,7 @@ mod test {
         derive rank = rank
         "#;
 
-        let query = resolve(parse(query).unwrap()).unwrap();
-
-        let sql_ast = compile(query, None).unwrap();
+        let sql_ast = crate::test::compile(query).unwrap();
 
         assert_snapshot!(sql_ast, @r###"
         WITH table_1 AS (
@@ -710,7 +680,7 @@ mod test {
         filter (average bar) > 3
         "#;
 
-        assert_snapshot!(crate::compile(query).unwrap(), @r###"
+        assert_snapshot!(crate::test::compile(query).unwrap(), @r###"
         WITH table_1 AS (
           SELECT
             *,
diff --git a/prql-js/src/lib.rs b/prql-js/src/lib.rs
index abfe8f42..399349b6 100644
--- a/prql-js/src/lib.rs
+++ b/prql-js/src/lib.rs
@@ -49,8 +49,10 @@ pub fn rq_to_sql(rq_json: &str) -> Option<String> {
 #[wasm_bindgen]
 #[derive(Clone)]
 pub struct CompileOptions {
-    /// True for passing generated SQL string trough a formatter that splits
+    /// Pass generated SQL string trough a formatter that splits it
     /// into multiple lines and prettifies indentation and spacing.
+    ///
+    /// Defaults to true.
     pub format: bool,
 
     /// Target dialect you want to compile for.
@@ -65,6 +67,11 @@ pub struct CompileOptions {
     /// If None is used, `sql_dialect` flag from query definition is used.
     /// If it does not exist, [Dialect::Generic] is used.
     pub dialect: Option<Dialect>,
+
+    /// Emits the compiler signature as a comment after generated SQL
+    ///
+    /// Defaults to true.
+    pub signature_comment: bool,
 }
 
 #[wasm_bindgen]
@@ -72,6 +79,7 @@ pub fn default_compile_options() -> CompileOptions {
     CompileOptions {
         format: true,
         dialect: None,
+        signature_comment: true,
     }
 }
 
@@ -80,6 +88,7 @@ impl From<CompileOptions> for prql_compiler::sql::Options {
         prql_compiler::sql::Options {
             format: o.format,
             dialect: o.dialect.map(From::from),
+            signature_comment: o.signature_comment,
         }
     }
 }
diff --git a/prql-python/src/lib.rs b/prql-python/src/lib.rs
index 9b806b70..ef2a4f3f 100644
--- a/prql-python/src/lib.rs
+++ b/prql-python/src/lib.rs
@@ -53,8 +53,10 @@ fn prql_python(_py: Python, m: &PyModule) -> PyResult<()> {
 #[pyclass]
 #[derive(Clone)]
 pub struct CompileOptions {
-    /// True for passing generated SQL string trough a formatter that splits
+    /// Pass generated SQL string trough a formatter that splits it
     /// into multiple lines and prettifies indentation and spacing.
+    ///
+    /// Defaults to true.
     pub format: bool,
 
     /// Target dialect you want to compile for.
@@ -69,6 +71,11 @@ pub struct CompileOptions {
     /// If None is used, `sql_dialect` flag from query definition is used.
     /// If it does not exist, [Dialect::Generic] is used.
     pub dialect: Option<Dialect>,
+
+    /// Emits the compiler signature as a comment after generated SQL
+    ///
+    /// Defaults to true.
+    pub signature_comment: bool,
 }
 
 impl From<CompileOptions> for prql_compiler::sql::Options {
@@ -76,6 +83,7 @@ impl From<CompileOptions> for prql_compiler::sql::Options {
         prql_compiler::sql::Options {
             format: o.format,
             dialect: o.dialect,
+            signature_comment: o.signature_comment,
         }
     }
 }
@@ -87,8 +95,14 @@ mod test {
 
     #[test]
     fn parse_for_python() {
+        let opts = Some(CompileOptions {
+            format: true,
+            dialect: None,
+            signature_comment: false,
+        });
+
         assert_eq!(
-            compile("from employees | filter (age | in 20..30)", None).unwrap(),
+            compile("from employees | filter (age | in 20..30)", opts).unwrap(),
             "SELECT\n  *\nFROM\n  employees\nWHERE\n  age BETWEEN 20\n  AND 30"
         );
     }


Test patch:
diff --git a/book/tests/snapshot.rs b/book/tests/snapshot.rs
index da2dae13..6b219b86 100644
--- a/book/tests/snapshot.rs
+++ b/book/tests/snapshot.rs
@@ -118,6 +118,13 @@ fn write_reference_prql() -> Result<()> {
     Ok(())
 }
 
+fn compile(prql: &str) -> Result<String, ErrorMessages> {
+    prql_to_pl(prql)
+        .and_then(pl_to_rq)
+        .and_then(|rq| rq_to_sql(rq, sql::Options::default().no_signature().some()))
+        .map_err(|e| e.composed("", prql, false))
+}
+
 /// Snapshot the output of each example.
 fn run_reference_prql() {
     glob!("prql/**/*.prql", |path| {
diff --git a/prql-compiler/src/test.rs b/prql-compiler/src/test.rs
index 6b7919ab..67703fe9 100644
--- a/prql-compiler/src/test.rs
+++ b/prql-compiler/src/test.rs
@@ -1,8 +1,16 @@
 //! Simple tests for "this PRQL creates this SQL" go here.
-use super::*;
-use crate::parser::parse;
+// use super::*;
+use crate::{parser::parse, sql};
 use insta::{assert_display_snapshot, assert_snapshot};
 
+pub fn compile(prql: &str) -> Result<String, crate::ErrorMessages> {
+    crate::parser::parse(prql)
+        .and_then(crate::semantic::resolve)
+        .and_then(|rq| sql::compile(rq, sql::Options::default().no_signature().some()))
+        .map_err(crate::error::downcast)
+        .map_err(|e| e.composed("", prql, false))
+}
+
 #[test]
 fn test_stdlib() {
     assert_snapshot!(compile(r###"
@@ -36,8 +44,8 @@ fn test_stdlib() {
 
 #[test]
 fn json_of_test() {
-    let json = prql_to_pl("from employees | take 10")
-        .and_then(json::from_pl)
+    let json = crate::prql_to_pl("from employees | take 10")
+        .and_then(crate::json::from_pl)
         .unwrap();
     // Since the AST is so in flux right now just test that the brackets are present
     assert_eq!(json.chars().next().unwrap(), '[');
@@ -1136,15 +1144,15 @@ derive mng_name = s"managers.first_name || ' ' || managers.last_name"
 select [mng_name, managers.gender, salary_avg, salary_sd]"#;
 
     let sql_from_prql = parse(original_prql)
-        .and_then(semantic::resolve)
+        .and_then(crate::semantic::resolve)
         .and_then(|rq| sql::compile(rq, None))
         .unwrap();
 
-    let sql_from_json = prql_to_pl(original_prql)
-        .and_then(json::from_pl)
-        .and_then(|json| json::to_pl(&json))
-        .and_then(pl_to_rq)
-        .and_then(|rq| rq_to_sql(rq, None))
+    let sql_from_json = crate::prql_to_pl(original_prql)
+        .and_then(crate::json::from_pl)
+        .and_then(|json| crate::json::to_pl(&json))
+        .and_then(crate::pl_to_rq)
+        .and_then(|rq| crate::rq_to_sql(rq, None))
         .unwrap();
 
     assert_eq!(sql_from_prql, sql_from_json);
@@ -2167,3 +2175,20 @@ fn test_switch() {
     "###
     );
 }
+
+#[test]
+fn test_sql_options() {
+    let sql = crate::compile("from x").unwrap();
+
+    assert!(sql.contains('\n'));
+    assert!(sql.contains("-- Generated by"));
+
+    let opts = sql::Options::default().no_signature().no_format().some();
+    let sql = crate::parser::parse("from  x")
+        .and_then(crate::semantic::resolve)
+        .and_then(|rq| sql::compile(rq, opts))
+        .unwrap();
+
+    assert!(!sql.contains('\n'));
+    assert!(!sql.contains("-- Generated by"));
+}