Repository: prql/prql
Pull request #431 (2022-04-27 18:56:03)

Reference book tweaks & examples
Added examples to docs sidebar, tweaks to CSS

This was the problem:
![image](https://user-images.githubusercontent.com/11072061/165599202-dee14880-f861-493d-8fb8-6fc81684ea8a.png)

Now it looks like this:
![image](https://user-images.githubusercontent.com/11072061/165599341-b9ef9965-d083-4c7d-8d4b-10adfb77e83f.png)


max-sixty: I'm sure I've done things badly and there are ways of improving, but I'm not sure this looks great either:

<img width="1070" alt="image" src="https://user-images.githubusercontent.com/5635139/165627017-ce4761c3-a0fc-4d11-8ed0-35f9980c5f2a.png">

and then if I scroll right:

<img width="1074" alt="image" src="https://user-images.githubusercontent.com/5635139/165627070-23cf824e-a350-4fa8-88f4-1ba652e378df.png">

max-sixty: The existing mode has a scrolling issue with wide columns:

<img width="1078" alt="image" src="https://user-images.githubusercontent.com/5635139/165627508-9530f409-6be6-41d5-9448-8748f992b407.png">

I had tried widening the max-width, which helped, but not completely...

One option is to adjust the examples by "inlining" the comments so the columns are much less wide.

---

One difference between out two views on the current impl — it looks like if one column has less text, the table background doesn't fill down — is that correct?

I tried it on Chrome (I generally use Firefox) and I see something similar — an ugly scroll bar across the cell. But the background does continue after.

Your screenshot:
<img width="594" alt="image" src="https://user-images.githubusercontent.com/5635139/165629062-56870447-0445-4df1-892b-bf8bb67b1f00.png">



My chrome screenshot:
<img width="1074" alt="image" src="https://user-images.githubusercontent.com/5635139/165628826-05892a5e-e112-422f-9392-7ae61123896f.png">


[My firefox screenshot is above]

Patch:
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 90fc4427..2278c9f8 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -29,13 +29,16 @@ jobs:
         with:
           github_token: ${{ secrets.GITHUB_TOKEN }}
           publish_dir: reference/book/
+
+  build-python-wheels:
+    uses: ./.github/workflows/python.yaml
   publish-to-pypi:
     name: Release
     runs-on: ubuntu-latest
     # Currently excluded since we need to use the PYPI_API_TOKEN
     # if: "startsWith(github.ref, 'refs/tags/')"
     if: false
-    needs: [build-python]
+    needs: [build-python-wheels]
     steps:
       - uses: actions/download-artifact@v2
         with:
diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml
index 3a00a051..81053af2 100644
--- a/.github/workflows/pull-request.yaml
+++ b/.github/workflows/pull-request.yaml
@@ -9,9 +9,16 @@ on:
     # Add `labeled`, so we can trigger a new run by adding a `ci-run-all-tests`
     # label, which we then use to trigger a `test-all` run.
     types: [opened, reopened, synchronize, labeled]
+  # While most of these tests are covered by `test-all`, we need to build the cache.
+  push:
+    branches:
+      - main
 
 concurrency:
-  group: ${{ github.workflow }}-${{ github.ref }}
+  # This adds `job` to the key so when running full tests, we don't cancel the
+  # overlapping jobs, since GH relies on them passing before allowing a PR to
+  # pass.
+  group: ${{ github.workflow }}-${{ github.ref }}-${{ github.job }}
   cancel-in-progress: true
 
 jobs:
diff --git a/.github/workflows/python.yaml b/.github/workflows/python.yaml
index caa720f4..8bcf7070 100644
--- a/.github/workflows/python.yaml
+++ b/.github/workflows/python.yaml
@@ -1,5 +1,5 @@
-# TODO: Cache doesn't seem to work. So we only run on PRs when files in the
-# `prql-python` path change so we avoid slowing down most builds.
+# TODO: Cargo's cache doesn't seem to work. So we only run on PRs when files in
+# the `prql-python` path change so we avoid slowing down most builds.
 
 name: python
 
diff --git a/prql-compiler/src/ast/ast_fold.rs b/prql-compiler/src/ast/ast_fold.rs
index 6ecc2962..b1e4ce7e 100644
--- a/prql-compiler/src/ast/ast_fold.rs
+++ b/prql-compiler/src/ast/ast_fold.rs
@@ -115,6 +115,11 @@ pub fn fold_item<T: ?Sized + AstFold>(fold: &mut T, item: Item) -> Result<Item>
         // None of these capture variables, so we don't need to replace
         // them.
         Item::String(_) | Item::Raw(_) | Item::Interval(_) => item,
+        Item::Windowed(window) => Item::Windowed(Windowed {
+            expr: Box::new(fold.fold_node(*window.expr)?),
+            group: fold.fold_nodes(window.group)?,
+            sort: fold.fold_column_sorts(window.sort)?,
+        }),
     })
 }
 
@@ -162,9 +167,12 @@ pub fn fold_transform<T: ?Sized + AstFold>(
     match transformation {
         Transform::From(table) => Ok(Transform::From(fold.fold_table_ref(table)?)),
 
-        Transform::Derive(select) => Ok(Transform::Derive(fold.fold_select(select)?)),
-        Transform::Select(select) => Ok(Transform::Select(fold.fold_select(select)?)),
-        Transform::Aggregate(select) => Ok(Transform::Aggregate(fold.fold_select(select)?)),
+        Transform::Derive(assigns) => Ok(Transform::Derive(fold.fold_nodes(assigns)?)),
+        Transform::Select(assigns) => Ok(Transform::Select(fold.fold_nodes(assigns)?)),
+        Transform::Aggregate { assigns, by } => Ok(Transform::Aggregate {
+            assigns: fold.fold_nodes(assigns)?,
+            by: fold.fold_nodes(by)?,
+        }),
 
         Transform::Filter(items) => Ok(Transform::Filter(fold.fold_nodes(items)?)),
         Transform::Sort(items) => Ok(Transform::Sort(fold.fold_column_sorts(items)?)),
diff --git a/prql-compiler/src/ast/item.rs b/prql-compiler/src/ast/item.rs
index f05c47a1..b72b6222 100644
--- a/prql-compiler/src/ast/item.rs
+++ b/prql-compiler/src/ast/item.rs
@@ -28,6 +28,7 @@ pub enum Item {
     SString(Vec<InterpolateItem>),
     FString(Vec<InterpolateItem>),
     Interval(Interval),
+    Windowed(Windowed),
 }
 
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
@@ -44,6 +45,23 @@ pub struct FuncCall {
     pub named_args: HashMap<Ident, Box<Node>>,
 }
 
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
+pub struct Windowed {
+    pub expr: Box<Node>,
+    pub group: Vec<Node>,
+    pub sort: Vec<ColumnSort<Node>>,
+    // pub frame: Vec<Node>,
+}
+
+impl Windowed {
+    pub fn new(node: Node) -> Self {
+        Windowed {
+            expr: Box::new(node),
+            group: vec![],
+            sort: vec![],
+        }
+    }
+}
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
 pub struct Pipeline {
     pub value: Option<Box<Node>>,
@@ -220,6 +238,9 @@ impl Display for Item {
             Item::Interval(i) => {
                 write!(f, "{}{}", i.n, i.unit)?;
             }
+            Item::Windowed(w) => {
+                write!(f, "{:?}", w.expr)?;
+            }
         }
         Ok(())
     }
diff --git a/prql-compiler/src/ast/mod.rs b/prql-compiler/src/ast/mod.rs
index 67a3c8f9..4ae17613 100644
--- a/prql-compiler/src/ast/mod.rs
+++ b/prql-compiler/src/ast/mod.rs
@@ -33,6 +33,12 @@ pub struct Node {
 }
 
 impl Node {
+    pub fn new_ident<S: ToString>(name: S, declared_at: usize) -> Node {
+        let mut node: Node = Item::Ident(name.to_string()).into();
+        node.declared_at = Some(declared_at);
+        node
+    }
+
     /// Return an error if this is named expression.
     pub fn discard_name(self) -> Result<Node, Error> {
         // TODO: replace this function with a prior type checking
diff --git a/prql-compiler/src/ast/query.rs b/prql-compiler/src/ast/query.rs
index 1d027e95..e45e4221 100644
--- a/prql-compiler/src/ast/query.rs
+++ b/prql-compiler/src/ast/query.rs
@@ -44,10 +44,13 @@ pub struct Table {
 #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr)]
 pub enum Transform {
     From(TableRef),
-    Select(Select),
+    Select(Vec<Node>),
     Filter(Vec<Node>),
-    Derive(Select),
-    Aggregate(Select),
+    Derive(Vec<Node>),
+    Aggregate {
+        assigns: Vec<Node>,
+        by: Vec<Node>,
+    },
     Sort(Vec<ColumnSort<Node>>),
     Take(i64),
     Join {
diff --git a/prql-compiler/src/parser.rs b/prql-compiler/src/parser.rs
index 6caeafbb..35b2b105 100644
--- a/prql-compiler/src/parser.rs
+++ b/prql-compiler/src/parser.rs
@@ -170,7 +170,7 @@ fn ast_of_parse_tree(pairs: Pairs<Rule>) -> Result<Vec<Node>> {
                     })
                 }
                 Rule::ident => Item::Ident(pair.as_str().to_string()),
-                Rule::string_literal => Item::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(),
@@ -253,7 +253,7 @@ fn ast_of_interpolate_items(pair: Pair<Rule>) -> Result<Vec<InterpolateItem>> {
     pair.into_inner()
         .map(|x| {
             Ok(match x.as_rule() {
-                Rule::interpolate_string => InterpolateItem::String(x.as_str().to_string()),
+                Rule::interpolate_string_inner => InterpolateItem::String(x.as_str().to_string()),
                 _ => InterpolateItem::Expr(Box::new(
                     ast_of_parse_tree(x.into_inner())?.into_expr().into(),
                 )),
@@ -275,10 +275,10 @@ mod test {
 
     #[test]
     fn test_parse_string() -> Result<()> {
-        assert_debug_snapshot!(parse_tree_of_str(r#"" U S A ""#, Rule::string_literal)?, @r###"
+        assert_debug_snapshot!(parse_tree_of_str(r#"" U S A ""#, Rule::string)?, @r###"
         [
             Pair {
-                rule: string_literal,
+                rule: string,
                 span: Span {
                     str: "\" U S A \"",
                     start: 0,
@@ -298,32 +298,32 @@ mod test {
             },
         ]
         "###);
-        let double_quoted_ast = ast_of_string(r#"" U S A ""#, Rule::string_literal)?;
+        let double_quoted_ast = ast_of_string(r#"" U S A ""#, Rule::string)?;
         assert_yaml_snapshot!(double_quoted_ast, @r###"
         ---
         String: " U S A "
         "###);
 
-        let single_quoted_ast = ast_of_string(r#"' U S A '"#, Rule::string_literal)?;
+        let single_quoted_ast = ast_of_string(r#"' U S A '"#, Rule::string)?;
         assert_eq!(single_quoted_ast, double_quoted_ast);
 
         // Single quotes within double quotes should produce a string containing
         // the single quotes (and vice versa).
-        assert_yaml_snapshot!(ast_of_string(r#""' U S A '""#, Rule::string_literal)? , @r###"
+        assert_yaml_snapshot!(ast_of_string(r#""' U S A '""#, Rule::string)? , @r###"
         ---
         String: "' U S A '"
         "###);
-        assert_yaml_snapshot!(ast_of_string(r#"'" U S A "'"#, Rule::string_literal)? , @r###"
+        assert_yaml_snapshot!(ast_of_string(r#"'" U S A "'"#, Rule::string)? , @r###"
         ---
         String: "\" U S A \""
         "###);
 
-        assert!(ast_of_string(r#"" U S A"#, Rule::string_literal).is_err());
-        assert!(ast_of_string(r#"" U S A '"#, Rule::string_literal).is_err());
+        assert!(ast_of_string(r#"" U S A"#, Rule::string).is_err());
+        assert!(ast_of_string(r#"" U S A '"#, Rule::string).is_err());
 
         // Escapes get passed through (the insta snapshot has them escaped I
         // think, which isn't that clear, so repeated below).
-        let escaped_string = ast_of_string(r#"" \U S A ""#, Rule::string_literal)?;
+        let escaped_string = ast_of_string(r#"" \U S A ""#, Rule::string)?;
         assert_yaml_snapshot!(escaped_string, @r###"
         ---
         String: " \\U S A "
@@ -332,16 +332,44 @@ mod test {
 
         // Currently we don't allow escaping closing quotes — because it's not
         // trivial to do in pest, and I'm not sure it's a great idea either — we
-        // should arguably encourage r-strings. (Though no objection if someone
-        // wants to implement it, this test is recording current behavior rather
-        // than maintaining a contract).
-        let escaped_quotes = ast_of_string(r#"" Canada \""#, Rule::string_literal)?;
+        // should arguably encourage multiline-strings. (Though no objection if
+        // someone wants to implement it, this test is recording current
+        // behavior rather than maintaining a contract).
+        let escaped_quotes = ast_of_string(r#"" Canada \""#, Rule::string)?;
         assert_yaml_snapshot!(escaped_quotes, @r###"
         ---
         String: " Canada \\"
         "###);
         assert_eq!(escaped_quotes.item.as_string().unwrap(), r#" Canada \"#);
 
+        let multi_double = ast_of_string(
+            r#""""
+''
+Canada
+"
+
+""""#,
+            Rule::string,
+        )?;
+        assert_yaml_snapshot!(multi_double, @r###"
+        ---
+        String: "\n''\nCanada\n\"\n\n"
+        "###);
+
+        let multi_single = ast_of_string(
+            r#"'''
+Canada
+"
+"""
+
+'''"#,
+            Rule::string,
+        )?;
+        assert_yaml_snapshot!(multi_single, @r###"
+        ---
+        String: "\nCanada\n\"\n\"\"\"\n\n"
+        "###);
+
         Ok(())
     }
 
@@ -1378,4 +1406,27 @@ select [
                     named_args: {}
         "###);
     }
+
+    #[test]
+    fn test_multiline_string() {
+        assert_yaml_snapshot!(parse(r###"
+        derive x: r#"r-string test"#
+        "###).unwrap(), @r###"
+        ---
+        version: ~
+        dialect: Generic
+        nodes:
+          - Pipeline:
+              value: ~
+              functions:
+                - FuncCall:
+                    name: derive
+                    args:
+                      - NamedExpr:
+                          name: x
+                          expr:
+                            Ident: r
+                    named_args: {}
+        "### )
+    }
 }
diff --git a/prql-compiler/src/prql.pest b/prql-compiler/src/prql.pest
index 09c25bbf..9a3a7b50 100644
--- a/prql-compiler/src/prql.pest
+++ b/prql-compiler/src/prql.pest
@@ -19,7 +19,7 @@ func_def = { "func" ~ func_def_flags ~ ident ~ func_def_params ~ "=" ~ expr }
 
 // TODO: consider some syntax for these func "type" args; currently they're only
 // differentiated from params by their position
-func_def_flags = @{ ("transform" | "aggregation" | "window" | "")? }
+func_def_flags = @{ ("transform" | "aggregation" | "window" | "native" | "")? }
 func_def_params = { ( named_term | ident )* }
 table = { "table" ~ ident ~ "=" ~ nested_pipeline }
 
@@ -72,7 +72,7 @@ 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_literal }
+literal = _{ interval | number | string}
 list = { "[" ~ (NEWLINE* ~ named_expr_call ~ ("," ~ NEWLINE* ~ named_expr_call )* ~ ","?)? ~ NEWLINE* ~ "]" }
 nested_expr = _{ "(" ~ expr_call ~ ")" }
 nested_pipeline = { "(" ~ expr_call? ~ pipe ~ pipeline ~ NEWLINE* ~ ")" }
@@ -82,9 +82,9 @@ nested_pipeline = { "(" ~ expr_call? ~ pipe ~ pipeline ~ NEWLINE* ~ ")" }
 // https://pest.rs/book/examples/rust/literals.html
 
 // We need to have a non-silent rule which contains the quotes
-// — `string_literal` in this case — because of
+// — `string` in this case — because of
 // https://github.com/pest-parser/pest/issues/583. Then when converting to AST,
-// we only keep the `string_inner` and discard the `string_literal` given it contains
+// we only keep the `string_inner` and discard the `string` given it contains
 // the quotes.
 //
 // TODO: I'm still a bit unclear how preceeding and trailing spaces are working
@@ -93,9 +93,11 @@ nested_pipeline = { "(" ~ expr_call? ~ pipe ~ pipeline ~ NEWLINE* ~ ")" }
 // atomic operator. We have some rudimentary tests for these.
 
 quote = _{ "\"" | "'" }
+multi_quote = _{ "\""{3,} | "'"{3,} }
 // PEEK refers to the opening quote; either `"` or `'`.
 string_inner = { ( !( PEEK ) ~ ANY )+ }
-string_literal = ${ PUSH(quote) ~ string_inner ~ POP }
+// Either > 3 quotes, or just one. Currently both of those can be multiline.
+string= ${ ( PUSH(multi_quote) | PUSH(quote) ) ~ string_inner ~ POP }
 
 number = ${ ( ASCII_DIGIT )+ ~ ("." ~ ( ASCII_DIGIT )+)? }
 
@@ -112,9 +114,10 @@ operator_add = ${ "+" | "-" }
 operator_compare = ${ "==" | "!=" | "=" | ">" | "<" | ">=" | "<=" }
 operator_logical = ${ ("and" | "or" | "not") ~ &WHITESPACE }
 
-s_string = ${ "s" ~ PUSH(quote) ~ ( interpolate_string | ( "{" ~ expr_call ~ "}" ))* ~ POP }
-f_string = ${ "f" ~ PUSH(quote) ~ ( interpolate_string | ( "{" ~ expr_call ~ "}" ))* ~ POP }
-interpolate_string = { ( !( PEEK | "{" ) ~ ANY )+ }
+// TODO: allow a raw s-string / f-string (but not an s-f string...)
+s_string = ${ "s" ~ PUSH(quote) ~ ( interpolate_string_inner | ( "{" ~ expr_call ~ "}" ))* ~ POP }
+f_string = ${ "f" ~ PUSH(quote) ~ ( interpolate_string_inner | ( "{" ~ expr_call ~ "}" ))* ~ POP }
+interpolate_string_inner = { ( !( PEEK | "{" ) ~ ANY )+ }
 
 interval_kind = { "microseconds" | "milliseconds" | "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years" }
 interval = ${ number ~ interval_kind }
diff --git a/prql-compiler/src/semantic/frame.rs b/prql-compiler/src/semantic/frame.rs
index 2aee4f85..f9e216e4 100644
--- a/prql-compiler/src/semantic/frame.rs
+++ b/prql-compiler/src/semantic/frame.rs
@@ -54,13 +54,13 @@ impl Frame {
                 self.columns.push(FrameColumn::All(table_id));
             }
 
-            Transform::Select(select) => {
+            Transform::Select(assigns) => {
                 self.columns.clear();
 
-                self.apply_assigns(&select.assigns);
+                self.apply_assigns(assigns);
             }
-            Transform::Derive(select) => {
-                self.apply_assigns(&select.assigns);
+            Transform::Derive(assigns) => {
+                self.apply_assigns(assigns);
             }
             Transform::Group { pipeline, .. } => {
                 let pipeline = pipeline.item.as_pipeline().unwrap();
@@ -68,14 +68,23 @@ impl Frame {
                     self.apply_transform(transform)?;
                 }
             }
-            Transform::Aggregate(select) => {
+            Transform::Aggregate { assigns, by } => {
+                let old_columns = self.columns.clone();
+
                 self.columns.clear();
 
-                for by in &select.group {
-                    self.push_column(None, by.declared_at.unwrap());
+                for b in by {
+                    let id = b.declared_at.unwrap();
+                    let col = old_columns.iter().find(|c| c == &&id);
+                    let name = col.and_then(|c| match c {
+                        FrameColumn::Named(n, _) => Some(n.clone()),
+                        _ => None,
+                    });
+
+                    self.push_column(name, id);
                 }
 
-                self.apply_assigns(&select.assigns);
+                self.apply_assigns(assigns);
             }
             Transform::Join { with, filter, .. } => {
                 let table_id = with
@@ -136,7 +145,7 @@ impl Frame {
     }
 }
 
-fn extract_sorts(sort: &[ColumnSort]) -> Result<Vec<ColumnSort<usize>>> {
+pub(super) fn extract_sorts(sort: &[ColumnSort]) -> Result<Vec<ColumnSort<usize>>> {
     sort.iter()
         .map(|s| {
             Ok(ColumnSort {
diff --git a/prql-compiler/src/semantic/resolver.rs b/prql-compiler/src/semantic/resolver.rs
index a320d970..bc68add5 100644
--- a/prql-compiler/src/semantic/resolver.rs
+++ b/prql-compiler/src/semantic/resolver.rs
@@ -1,12 +1,13 @@
 use std::collections::HashSet;
 
-use anyhow::Result;
+use anyhow::{bail, Result};
 use itertools::Itertools;
 
 use crate::ast::ast_fold::*;
 use crate::error::{Error, Reason, WithErrorInfo};
 use crate::{ast::*, Declaration};
 
+use super::frame::extract_sorts;
 use super::transforms;
 use super::Context;
 
@@ -33,6 +34,8 @@ pub struct Resolver {
     within_curry: bool,
 
     within_group: Vec<usize>,
+
+    sorted: Vec<ColumnSort<usize>>,
 }
 impl Resolver {
     fn new(context: Context) -> Self {
@@ -40,6 +43,7 @@ impl Resolver {
             context,
             within_curry: false,
             within_group: vec![],
+            sorted: vec![],
         }
     }
 }
@@ -103,12 +107,21 @@ impl AstFold for Resolver {
                     .with_span(node.span)?;
 
                 // fold (and cast if this is a transform)
-                if let Some(FuncKind::Transform) = func_def.kind {
-                    let transform = transforms::cast_transform(func_call, node.span)?;
+                match func_def.kind {
+                    Some(FuncKind::Transform) => {
+                        let transform = transforms::cast_transform(func_call, node.span)?;
+
+                        Item::Transform(self.fold_transform(transform)?)
+                    }
+                    Some(FuncKind::Window) => {
+                        // wrap into Windowed
+                        let mut expr: Node = Item::FuncCall(self.fold_func_call(func_call)?).into();
+                        expr.declared_at = node.declared_at;
+                        node.declared_at = None;
 
-                    Item::Transform(self.fold_transform(transform)?)
-                } else {
-                    Item::FuncCall(self.fold_func_call(func_call)?)
+                        Item::Windowed(Windowed::new(expr))
+                    }
+                    _ => Item::FuncCall(self.fold_func_call(func_call)?),
                 }
             }
 
@@ -129,8 +142,10 @@ impl AstFold for Resolver {
     }
 
     fn fold_transform(&mut self, t: Transform) -> Result<Transform> {
-        let t = match t {
+        let mut t = match t {
             Transform::From(mut t) => {
+                self.sorted.clear();
+
                 self.context.scope.clear();
 
                 self.context.declare_table(&mut t);
@@ -138,34 +153,35 @@ impl AstFold for Resolver {
                 Transform::From(t)
             }
 
-            Transform::Select(mut select) => {
-                select.assigns = self.fold_assigns(select.assigns)?;
+            Transform::Select(assigns) => {
+                let assigns = self.fold_assigns(assigns)?;
                 self.context.scope.clear();
 
-                self.apply_context(&mut select)?;
-                Transform::Select(select)
+                Transform::Select(assigns)
             }
-            Transform::Derive(mut select) => {
-                select.assigns = self.fold_assigns(select.assigns)?;
+            Transform::Derive(assigns) => {
+                let assigns = self.fold_assigns(assigns)?;
 
-                self.apply_context(&mut select)?;
-                Transform::Derive(select)
+                Transform::Derive(assigns)
             }
             Transform::Group { by, pipeline } => {
                 let by = self.fold_nodes(by)?;
 
                 self.within_group = by.iter().filter_map(|n| n.declared_at).collect();
+                let sorted = self.sorted.clone();
+
                 let pipeline = Box::new(self.fold_node(*pipeline)?);
+
                 self.within_group = vec![];
+                self.sorted = sorted;
 
                 Transform::Group { by, pipeline }
             }
-            Transform::Aggregate(mut select) => {
-                select.assigns = self.fold_assigns(select.assigns)?;
+            Transform::Aggregate { assigns, by } => {
+                let assigns = self.fold_assigns(assigns)?;
                 self.context.scope.clear();
 
-                self.apply_context(&mut select)?;
-                Transform::Aggregate(select)
+                Transform::Aggregate { assigns, by }
             }
             Transform::Join {
                 side,
@@ -180,12 +196,22 @@ impl AstFold for Resolver {
                     filter: self.fold_join_filter(filter)?,
                 }
             }
+            Transform::Sort(sorts) => {
+                let sorts = self.fold_column_sorts(sorts)?;
+
+                self.sorted = extract_sorts(&sorts)?;
+
+                Transform::Sort(sorts)
+            }
             t => fold_transform(self, t)?,
         };
 
-        // if !self.within_group {
-        //     self.context.frame.apply_transform(&t)?;
-        // }
+        if !self.within_group.is_empty() {
+            self.apply_group(&mut t)?;
+        }
+        if !self.sorted.is_empty() {
+            self.apply_sort(&mut t)?;
+        }
 
         Ok(t)
     }
@@ -282,15 +308,78 @@ impl Resolver {
         })
     }
 
-    fn apply_context(&self, select: &mut Select) -> Result<()> {
-        select.group = (self.within_group)
+    fn apply_group(&mut self, t: &mut Transform) -> Result<()> {
+        const REF: &str = "<ref>";
+
+        let group_by: Vec<_> = (self.within_group)
             .iter()
-            .map(|id| {
-                let mut node: Node = Item::Ident("<un-materialized>".to_string()).into();
-                node.declared_at = Some(*id);
-                node
-            })
+            .map(|id| Node::new_ident(REF, *id))
             .collect();
+
+        match t {
+            Transform::Select(assigns) | Transform::Derive(assigns) => {
+                for assign in assigns {
+                    let id = assign.declared_at.unwrap();
+
+                    // try to update existing Windowed
+                    let decl = self.context.declarations.get_mut(id);
+                    if let Some((Declaration::Expression(node), _)) = decl {
+                        if let Item::Windowed(window) = &mut node.item {
+                            window.group = group_by.clone();
+                            continue;
+                        }
+                    }
+
+                    // wrap with new Windowed declaration
+                    let windowed = Windowed {
+                        expr: Box::new(Node::new_ident(REF, id)),
+                        group: group_by.clone(),
+                        sort: vec![],
+                    };
+                    let decl = Declaration::Expression(Box::new(Item::Windowed(windowed).into()));
+                    let window_id = self.context.declare(decl, None);
+                    assign.declared_at = Some(window_id);
+                }
+            }
+            Transform::Aggregate { by, .. } => {
+                *by = group_by;
+            }
+            Transform::Sort(_) => {}
+            _ => {
+                // TODO: attach span to this error
+                bail!(Error::new(Reason::Simple(format!(
+                    "transform `{}` is not allowed within group context",
+                    t.as_ref()
+                ))))
+            }
+        }
+        Ok(())
+    }
+
+    fn apply_sort(&mut self, t: &mut Transform) -> Result<()> {
+        match t {
+            Transform::Select(assigns) | Transform::Derive(assigns) => {
+                let sort: Vec<_> = (self.sorted)
+                    .iter()
+                    .map(|s| ColumnSort {
+                        column: Node::new_ident("<ref>", s.column),
+                        direction: s.direction.clone(),
+                    })
+                    .collect();
+
+                for assign in assigns {
+                    let id = assign.declared_at.unwrap();
+
+                    let decl = self.context.declarations.get_mut(id);
+                    if let Some((Declaration::Expression(node), _)) = decl {
+                        if let Item::Windowed(window) = &mut node.item {
+                            window.sort = sort.clone();
+                        }
+                    }
+                }
+            }
+            _ => {}
+        }
         Ok(())
     }
 
@@ -459,7 +548,7 @@ mod tests {
         assert_snapshot!(result, @r###"
         SELECT
           salary AS salary1,
-          salary1 + 1 AS salary2,
+          salary + 1 AS salary2,
           age
         FROM
           employees
@@ -532,4 +621,48 @@ mod tests {
           JOIN salaries USING(emp_no)
         "###);
     }
+
+    #[test]
+    fn test_applying_group_context() {
+        let prql = r#"
+        from employees
+        group last_name (
+            sort first_name
+            take 1
+        )
+        "#;
+        let result = parse(prql).and_then(resolve_and_translate);
+        assert!(result.is_err());
+
+        let prql = r#"
+        from employees
+        group last_name (
+            select first_name
+        )
+        "#;
+        let result = parse(prql).and_then(resolve_and_translate).unwrap();
+        assert_snapshot!(result, @r###"
+        SELECT
+          first_name OVER (PARTITION BY last_name) AS first_name
+        FROM
+          employees
+        "###);
+
+        let prql = r#"
+        from employees
+        group last_name (
+            aggregate count
+        )
+        "#;
+        let result = parse(prql).and_then(resolve_and_translate).unwrap();
+        assert_snapshot!(result, @r###"
+        SELECT
+          last_name,
+          COUNT(*) AS count
+        FROM
+          employees
+        GROUP BY
+          last_name
+        "###);
+    }
 }
diff --git a/prql-compiler/src/semantic/transforms.rs b/prql-compiler/src/semantic/transforms.rs
index 138513c5..68fdc79c 100644
--- a/prql-compiler/src/semantic/transforms.rs
+++ b/prql-compiler/src/semantic/transforms.rs
@@ -24,7 +24,7 @@ pub fn cast_transform(func_call: FuncCall, span: Option<Span>) -> Result<Transfo
         "select" => {
             let ([assigns], []) = unpack(func_call, [])?;
 
-            Transform::Select(Select::new(assigns.coerce_to_vec()))
+            Transform::Select(assigns.coerce_to_vec())
         }
         "filter" => {
             let ([filter], []) = unpack(func_call, [])?;
@@ -34,12 +34,15 @@ pub fn cast_transform(func_call: FuncCall, span: Option<Span>) -> Result<Transfo
         "derive" => {
             let ([assigns], []) = unpack(func_call, [])?;
 
-            Transform::Derive(Select::new(assigns.coerce_to_vec()))
+            Transform::Derive(assigns.coerce_to_vec())
         }
         "aggregate" => {
             let ([assigns], []) = unpack(func_call, [])?;
 
-            Transform::Aggregate(Select::new(assigns.coerce_to_vec()))
+            Transform::Aggregate {
+                assigns: assigns.coerce_to_vec(),
+                by: vec![],
+            }
         }
         "sort" => {
             let ([by], []) = unpack(func_call, [])?;
@@ -213,6 +216,9 @@ mod tests {
         ",
         )
         .unwrap();
+        // TODO: this test
+        assert!(resolve(query.nodes, context.clone()).is_err());
+        /*
         let (result, _) = resolve(query.nodes, context.clone()).unwrap();
         assert_yaml_snapshot!(result, @r###"
         ---
@@ -235,6 +241,7 @@ mod tests {
                           - Transform:
                               Take: 1
         "###);
+        */
 
         // oops, two arguments #339
         let query = parse(
@@ -278,7 +285,7 @@ mod tests {
                   From:
                     name: c_invoice
                     alias: ~
-                    declared_at: 58
+                    declared_at: 57
               - Transform:
                   Group:
                     by:
@@ -291,10 +298,8 @@ mod tests {
                               Aggregate:
                                 assigns:
                                   - Ident: "<unnamed>"
-                                group:
-                                  - Ident: "<un-materialized>"
-                                window: ~
-                                sort: ~
+                                by:
+                                  - Ident: "<ref>"
         "###);
     }
 }
diff --git a/prql-compiler/src/sql/materializer.rs b/prql-compiler/src/sql/materializer.rs
index df244b59..5b5a569f 100644
--- a/prql-compiler/src/sql/materializer.rs
+++ b/prql-compiler/src/sql/materializer.rs
@@ -15,24 +15,14 @@ use crate::semantic::{split_var_name, FrameColumn};
 
 /// Replaces all resolved functions and variables with their declarations.
 pub fn materialize(
-    pipeline: Pipeline,
+    mut pipeline: Pipeline,
     mut context: MaterializationContext,
     as_table: Option<usize>,
 ) -> Result<(Pipeline, MaterializedFrame, MaterializationContext)> {
-    // find frame
-    for transform in &pipeline.functions {
-        let transform = (transform.item)
-            .as_transform()
-            .ok_or_else(|| anyhow!("plain function in pipeline"))?;
-        context.frame.apply_transform(transform)?;
-    }
+    context.frame.sort.clear();
+    extract_frame(&mut pipeline, &mut context)?;
 
-    // init
-    let remove_namespaces = context.frame.tables.len() == 1;
-    let mut m = Materializer {
-        context,
-        remove_namespaces,
-    };
+    let mut m = Materializer::new(context);
 
     // materialize the query
     let pipeline = m.fold_pipeline(pipeline)?;
@@ -41,8 +31,7 @@ pub fn materialize(
     // materialize each of the columns
     let columns = m.anchor_columns(frame.columns, as_table)?;
 
-    // materialize each of the columns
-    let sort = m.lookup_sort(frame.sort)?;
+    let sort = m.materialize_sort(frame.sort)?;
 
     // rename tables for future pipelines
     if let Some(as_table) = as_table {
@@ -51,9 +40,26 @@ pub fn materialize(
 
     Ok((pipeline, MaterializedFrame { columns, sort }, m.context))
 }
+
+fn extract_frame(pipeline: &mut Pipeline, context: &mut MaterializationContext) -> Result<()> {
+    for f in &pipeline.functions {
+        let transform = (f.item)
+            .as_transform()
+            .ok_or_else(|| anyhow!("plain function in pipeline"))?;
+        context.frame.apply_transform(transform)?;
+    }
+
+    pipeline.functions.retain(|f| {
+        !matches!(
+            f.item.as_transform().unwrap(),
+            Transform::Select(_) | Transform::Derive(_) | Transform::Sort(_)
+        )
+    });
+    Ok(())
+}
 pub struct MaterializedFrame {
     pub columns: Vec<Node>,
-    pub sort: Vec<ColumnSort<Ident>>,
+    pub sort: Vec<ColumnSort>,
 }
 
 pub struct MaterializationContext {
@@ -101,13 +107,22 @@ pub struct Materializer {
 }
 
 impl Materializer {
+    fn new(context: MaterializationContext) -> Self {
+        Materializer {
+            remove_namespaces: context.frame.tables.len() == 1,
+            context,
+        }
+    }
+
     /// Looks up column declarations and replaces them with an identifiers.
     fn anchor_columns(
         &mut self,
         columns: Vec<FrameColumn>,
         as_table: Option<usize>,
     ) -> Result<Vec<Node>> {
-        columns
+        let mut to_replace = Vec::new();
+
+        let res = columns
             .into_iter()
             .map(|column| {
                 Ok(match column {
@@ -120,7 +135,7 @@ impl Materializer {
                             variable: name.clone(),
                             table: as_table,
                         };
-                        self.context.replace_declaration(id, decl);
+                        to_replace.push((id, decl));
 
                         emit_column_with_name(expr_node, name)
                     }
@@ -135,7 +150,13 @@ impl Materializer {
                     }
                 })
             })
-            .try_collect()
+            .collect::<Result<Vec<_>>>()?;
+
+        for (id, decl) in to_replace {
+            self.context.replace_declaration(id, decl);
+        }
+
+        Ok(res)
     }
 
     fn replace_tables(&mut self, tables: Vec<usize>, new_table: usize) {
@@ -147,14 +168,11 @@ impl Materializer {
 
     /// Folds the column and returns expression that can be used in select.
     /// Also returns column id and name if declaration should be replaced.
-    fn lookup_sort(&mut self, sort: Vec<ColumnSort<usize>>) -> Result<Vec<ColumnSort<Ident>>> {
+    fn materialize_sort(&mut self, sort: Vec<ColumnSort<usize>>) -> Result<Vec<ColumnSort>> {
         sort.into_iter()
             .map(|s| {
-                let ident = self.materialize_declaration(s.column)?;
-                let column = ident.item.into_ident()?;
-
                 Ok(ColumnSort {
-                    column,
+                    column: self.materialize_declaration(s.column)?,
                     direction: s.direction,
                 })
             })
@@ -196,11 +214,9 @@ impl Materializer {
         Ok(materialized)
     }
 
-    fn materialize_func_call(&mut self, node: &Node) -> Result<Node> {
-        let func_call = node.item.as_func_call().unwrap();
-
+    fn materialize_func_call(&mut self, func_call: &FuncCall, decl: Option<usize>) -> Result<Node> {
         // locate declaration
-        let func_dec = node.declared_at.ok_or_else(|| anyhow!("unresolved"))?;
+        let func_dec = decl.ok_or_else(|| anyhow!("unresolved"))?;
         let func_dec = &self.context.declarations[func_dec];
         let func_dec = func_dec.as_function().unwrap().clone();
 
@@ -253,13 +269,9 @@ impl AstFold for Materializer {
 
         Ok(match node.item {
             Item::FuncCall(func_call) => {
-                let func_call = Item::FuncCall(self.fold_func_call(func_call)?);
-                let func_call = Node {
-                    item: func_call,
-                    ..node
-                };
+                let func_call = self.fold_func_call(func_call)?;
 
-                self.materialize_func_call(&func_call)?
+                self.materialize_func_call(&func_call, node.declared_at)?
             }
 
             Item::Pipeline(p) => {
@@ -268,13 +280,23 @@ impl AstFold for Materializer {
 
                     let mut value = self.fold_node(*value)?;
 
-                    for mut func_call in p.functions {
-                        // The value from the previous pipeline becomes the final arg.
-                        if let Some(call) = func_call.item.as_func_call_mut() {
-                            call.args.push(value);
-                        }
+                    for function in p.functions {
+                        let (function, window) = if let Item::Windowed(w) = function.item {
+                            (*w.expr.clone(), Some(w))
+                        } else {
+                            (function, None)
+                        };
 
-                        value = self.materialize_func_call(&func_call)?;
+                        let mut func_call = (function.item.into_func_call())
+                            .map_err(|f| anyhow!("expected FuncCall, got {f:?}"))?;
+
+                        func_call.args.push(value);
+                        value = self.materialize_func_call(&func_call, function.declared_at)?;
+
+                        if let Some(mut w) = window {
+                            w.expr = Box::new(value);
+                            value = Item::Windowed(w).into();
+                        }
                     }
                     value
                 } else {
@@ -369,26 +391,14 @@ mod test {
             &to_string(&mat)?
         ),
         @r###"
-        @@ -9,8 +9,17 @@
-           - Transform:
-               Derive:
-                 assigns:
-        -          - Ident: gross_salary
-        -          - Ident: gross_cost
-        +          - Expr:
-        +              - Ident: salary
-        +              - Raw: +
-        +              - Ident: payroll_tax
-        +          - Expr:
-        +              - Expr:
-        +                  - Ident: salary
-        +                  - Raw: +
-        +                  - Ident: payroll_tax
-        +              - Raw: +
-        +              - Ident: benefits_cost
-                 group: []
-                 window: ~
-                 sort: ~
+        @@ -6,7 +6,3 @@
+                 name: employees
+                 alias: ~
+                 declared_at: 30
+        -  - Transform:
+        -      Derive:
+        -        - Ident: gross_salary
+        -        - Ident: gross_cost
         "###);
 
         Ok(())
@@ -493,7 +503,7 @@ take 20
         );
         assert!(!diff.is_empty());
         assert_display_snapshot!(diff, @r###"
-        @@ -7,7 +7,11 @@
+        @@ -7,5 +7,9 @@
          - Transform:
              Aggregate:
                assigns:
@@ -503,9 +513,7 @@ take 20
         +            - Expr:
         +                Ident: salary
         +            - String: )
-               group: []
-               window: ~
-               sort: ~
+               by: []
         "###);
 
         Ok(())
@@ -553,25 +561,6 @@ take 20
               name: a
               alias: ~
               declared_at: 35
-        - Transform:
-            Select:
-              assigns:
-                - Expr:
-                    - Expr:
-                        - Ident: b
-                        - Raw: /
-                        - SString:
-                            - String: lag_day_todo(
-                            - Expr:
-                                Ident: b
-                            - String: )
-                    - Raw: "-"
-                    - Raw: "1"
-                    - Raw: +
-                    - Ident: c
-              group: []
-              window: ~
-              sort: ~
         "###);
 
         Ok(())
@@ -595,7 +584,7 @@ take 20
         let (mat, _, _) = materialize(pipeline.clone(), context.into(), None)?;
 
         assert_snapshot!(diff(&to_string(&pipeline.functions)?, &to_string(&mat.functions)?), @r###"
-        @@ -7,8 +7,16 @@
+        @@ -7,6 +7,14 @@
          - Transform:
              Aggregate:
                assigns:
@@ -611,9 +600,7 @@ take 20
         +            - Expr:
         +                Ident: foo
         +            - String: )
-               group: []
-               window: ~
-               sort: ~
+               by: []
         "###);
 
         // Test it'll run the `sum foo` function first.
@@ -647,9 +634,7 @@ take 20
                         - String: )
                     - Raw: +
                     - Raw: "1"
-              group: []
-              window: ~
-              sort: ~
+              by: []
         "###);
 
         Ok(())
@@ -677,20 +662,6 @@ take 20
               name: foo_table
               alias: ~
               declared_at: 33
-        - Transform:
-            Derive:
-              assigns:
-                - Expr:
-                    - Ident: bar
-                    - Raw: +
-                    - Raw: "3"
-                - Expr:
-                    - Ident: bar
-                    - Raw: +
-                    - Raw: "1"
-              group: []
-              window: ~
-              sort: ~
         "###);
 
         Ok(())
@@ -726,9 +697,7 @@ take 20
                     - Expr:
                         Ident: salary
                     - String: )
-              group: []
-              window: ~
-              sort: ~
+              by: []
         "###
         );
         Ok(())
@@ -846,11 +815,9 @@ take 20
                                 - Expr:
                                     Ident: salary
                                 - String: )
-                          group:
+                          by:
                             - Ident: title
                             - Ident: emp_no
-                          window: ~
-                          sort: ~
         - Transform:
             Group:
               by:
@@ -871,10 +838,8 @@ take 20
                                           Ident: salary
                                       - String: )
                                 - String: )
-                          group:
+                          by:
                             - Ident: title
-                          window: ~
-                          sort: ~
         "###);
 
         Ok(())
@@ -909,19 +874,6 @@ take 20
               name: orders
               alias: ~
               declared_at: 30
-        - Transform:
-            Select:
-              assigns:
-                - Ident: customer_no
-                - Ident: gross
-                - Ident: tax
-                - Expr:
-                    - Ident: gross
-                    - Raw: "-"
-                    - Ident: tax
-              group: []
-              window: ~
-              sort: ~
         - Transform:
             Take: 20
         "###);
diff --git a/prql-compiler/src/sql/stdlib.prql b/prql-compiler/src/sql/stdlib.prql
index 28ebcdc1..fbd8ef64 100644
--- a/prql-compiler/src/sql/stdlib.prql
+++ b/prql-compiler/src/sql/stdlib.prql
@@ -28,5 +28,5 @@ func in range value = s"{value} BETWEEN {range}"
 func as column type = s"CAST({column} AS {type})"
 
 # Window functions
-func lag offset column = s"LAG({column}, {offset})"
-func rank column = s"RANK({column})"
+func window lag offset column = s"LAG({column}, {offset})"
+func window rank = s"RANK()"
diff --git a/prql-compiler/src/sql/translator.rs b/prql-compiler/src/sql/translator.rs
index 769d5356..01a5695a 100644
--- a/prql-compiler/src/sql/translator.rs
+++ b/prql-compiler/src/sql/translator.rs
@@ -14,7 +14,7 @@ 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,
+    TableWithJoins, Top, WindowSpec,
 };
 use sqlparser::ast::{DateTimeField, Value};
 use std::collections::HashMap;
@@ -242,34 +242,19 @@ fn sql_query_of_atomic_table(table: AtomicTable, dialect: &Dialect) -> Result<sq
         .min()
         .map(expr_of_i64);
 
-    // If there is sort transform in the pipeline
-    let sort = transforms.iter().any(|t| matches!(t, Transform::Sort(_)));
-    let order_by = if sort {
-        // Use sorting from the frame
-        (frame.sort.iter())
-            .map(|sort| OrderByExpr {
-                expr: Item::Ident(sort.column.clone()).try_into().unwrap(),
-                asc: if matches!(sort.direction, SortDirection::Asc) {
-                    None // default order is ASC, so there is no need to emit it
-                } else {
-                    Some(false)
-                },
-                nulls_first: None,
-            })
-            .collect()
-    } else {
-        vec![]
-    };
+    // Use sorting from the frame
+    let order_by = (frame.sort)
+        .into_iter()
+        .map(OrderByExpr::try_from)
+        .try_collect()?;
 
     let aggregate = transforms.get(aggregate_position);
 
     let group_bys: Vec<Node> = match aggregate {
-        Some(Transform::Aggregate(select)) => select.group.clone(),
+        Some(Transform::Aggregate { by, .. }) => by.clone(),
         None => vec![],
         _ => unreachable!("Expected an aggregate transformation"),
     };
-    let group_by = Item::List(group_bys).try_into()?;
-
     let dialect = dialect.handler();
 
     Ok(sql_ast::Query {
@@ -287,7 +272,7 @@ fn sql_query_of_atomic_table(table: AtomicTable, dialect: &Dialect) -> Result<sq
             from,
             lateral_views: vec![],
             selection: where_,
-            group_by,
+            group_by: try_into_exprs(group_bys)?,
             cluster_by: vec![],
             distribute_by: vec![],
             sort_by: vec![],
@@ -422,22 +407,6 @@ fn combine_filters(filters: Vec<Vec<Node>>) -> Vec<Node> {
         .collect()
 }
 
-impl TryFrom<Item> for SelectItem {
-    type Error = anyhow::Error;
-    fn try_from(item: Item) -> Result<Self> {
-        Ok(match item {
-            Item::Expr(_) | Item::SString(_) | Item::FString(_) | Item::Ident(_) | Item::Raw(_) => {
-                SelectItem::UnnamedExpr(Expr::try_from(item)?)
-            }
-            Item::NamedExpr(named) => SelectItem::ExprWithAlias {
-                alias: sql_ast::Ident::new(named.name),
-                expr: named.expr.item.try_into()?,
-            },
-            _ => bail!("Can't convert to SelectItem; {:?}", item),
-        })
-    }
-}
-
 fn expr_of_i64(number: i64) -> Expr {
     Expr::Value(Value::Number(
         number.to_string(),
@@ -452,7 +421,32 @@ fn top_of_expr(take: Expr) -> Top {
         percent: false,
     }
 }
+fn try_into_exprs(nodes: Vec<Node>) -> Result<Vec<Expr>> {
+    nodes
+        .into_iter()
+        .map(|x| x.item)
+        .map(Expr::try_from)
+        .try_collect()
+}
 
+impl TryFrom<Item> for SelectItem {
+    type Error = anyhow::Error;
+    fn try_from(item: Item) -> Result<Self> {
+        Ok(match item {
+            Item::Expr(_)
+            | Item::SString(_)
+            | Item::FString(_)
+            | Item::Ident(_)
+            | Item::Raw(_)
+            | Item::Windowed(_) => SelectItem::UnnamedExpr(Expr::try_from(item)?),
+            Item::NamedExpr(named) => SelectItem::ExprWithAlias {
+                alias: sql_ast::Ident::new(named.name),
+                expr: named.expr.item.try_into()?,
+            },
+            _ => bail!("Can't convert to SelectItem; {:?}", item),
+        })
+    }
+}
 impl TryFrom<Item> for Expr {
     type Error = anyhow::Error;
     fn try_from(item: Item) -> Result<Self> {
@@ -468,7 +462,7 @@ impl TryFrom<Item> for Expr {
                 Expr::Identifier(sql_ast::Ident::new(
                     items
                         .into_iter()
-                        .map(|node| TryInto::<Expr>::try_into(node.item))
+                        .map(|node| Expr::try_from(node.item))
                         .collect::<Result<Vec<Expr>>>()?
                         .iter()
                         .map(|x| x.to_string())
@@ -499,7 +493,7 @@ impl TryFrom<Item> for Expr {
                     .map(|s_string_item| match s_string_item {
                         InterpolateItem::String(string) => Ok(string),
                         InterpolateItem::Expr(node) => {
-                            TryInto::<Expr>::try_into(node.item).map(|expr| expr.to_string())
+                            Expr::try_from(node.item).map(|expr| expr.to_string())
                         }
                     })
                     .collect::<Result<Vec<String>>>()?
@@ -513,7 +507,7 @@ impl TryFrom<Item> for Expr {
                         InterpolateItem::String(string) => {
                             Ok(Expr::Value(sql_ast::Value::SingleQuotedString(string)))
                         }
-                        InterpolateItem::Expr(node) => TryInto::<Expr>::try_into(node.item),
+                        InterpolateItem::Expr(node) => Expr::try_from(node.item),
                     })
                     .map(|r| r.map(|e| FunctionArg::Unnamed(FunctionArgExpr::Expr(e))))
                     .collect::<Result<Vec<_>>>()?;
@@ -543,17 +537,55 @@ impl TryFrom<Item> for Expr {
                     fractional_seconds_precision: None,
                 })
             }
+            Item::Windowed(window) => {
+                let expr = Expr::try_from(window.expr.item)?;
+                let window = WindowSpec {
+                    partition_by: try_into_exprs(window.group)?,
+                    order_by: (window.sort)
+                        .into_iter()
+                        .map(OrderByExpr::try_from)
+                        .try_collect()?,
+                    window_frame: None,
+                };
+
+                Item::Ident(format!("{expr} OVER ({window})")).try_into()?
+            }
             _ => bail!("Can't convert to Expr; {item:?}"),
         })
     }
 }
-impl TryFrom<Item> for Vec<Expr> {
+impl TryFrom<FuncCall> for Function {
+    // I had an idea to for stdlib functions to have "native" keyword, which would prevent them from being
+    // resolved and materialized and would be passed to here. But that has little advantage over current approach.
+    // After some time when we know that current approach is good, this impl can be removed.
     type Error = anyhow::Error;
-    fn try_from(item: Item) -> Result<Self> {
-        match item {
-            Item::List(nodes) => Ok(nodes.into_iter().map(|x| x.item.try_into()).try_collect()?),
-            _ => Err(anyhow!("Can't convert to Vec<Expr>; {item:?}")),
-        }
+    fn try_from(func_call: FuncCall) -> Result<Self> {
+        let FuncCall { name, args, .. } = func_call;
+
+        Ok(Function {
+            name: ObjectName(vec![sql_ast::Ident::new(name)]),
+            args: args
+                .into_iter()
+                .map(|a| Expr::try_from(a.item))
+                .map(|e| e.map(|a| FunctionArg::Unnamed(FunctionArgExpr::Expr(a))))
+                .collect::<Result<Vec<_>>>()?,
+            over: None,
+            distinct: false,
+        })
+    }
+}
+impl TryFrom<ColumnSort> for OrderByExpr {
+    type Error = anyhow::Error;
+    fn try_from(sort: ColumnSort) -> Result<Self> {
+        Ok(OrderByExpr {
+            expr: sort.column.item.try_into()?,
+            asc: if matches!(sort.direction, SortDirection::Asc) {
+                None // default order is ASC, so there is no need to emit it
+            } else {
+                Some(false)
+            },
+            nulls_first: None,
+        })
     }
 }
 impl TryFrom<Item> for sql_ast::Ident {
@@ -652,11 +684,11 @@ SString:
 
     #[test]
     fn test_try_from_list_to_vec_expr() -> Result<()> {
-        let item = Item::List(vec![
+        let items = vec![
             Item::Ident("a".to_owned()).into(),
             Item::Ident("b".to_owned()).into(),
-        ]);
-        let expr: Vec<Expr> = item.try_into()?;
+        ];
+        let expr: Vec<Expr> = try_into_exprs(items)?;
         assert_debug_snapshot!(expr, @r###"
         [
             Identifier(
@@ -1219,4 +1251,148 @@ take 20
 
         Ok(())
     }
+
+    #[test]
+    fn test_window_functions() -> Result<()> {
+        let query: Query = parse(
+            r###"
+        from employees
+        group last_name (
+            derive count
+        )
+        "###,
+        )?;
+
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          employees.*,
+          COUNT(*) OVER (PARTITION BY last_name) AS count
+        FROM
+          employees
+        "###);
+
+        let query: Query = parse(
+            r###"
+        from co:cust_order
+        join ol:order_line [order_id]
+        derive [
+          order_month: s"TO_CHAR({co.order_date}, '%Y-%m')",
+          order_day: s"TO_CHAR({co.order_date}, '%Y-%m-%d')",
+        ]
+        group [order_month, order_day] (
+          aggregate [
+            num_orders: s"COUNT(DISTINCT {co.order_id})",
+            num_books: count ol.book_id,
+            total_price: sum ol.price,
+          ]
+        )
+        sort order_day # order:asc
+        group [order_month] (
+          derive [running_total_num_books: sum num_books]
+        )
+        derive [num_books_last_week: lag 7 num_books]
+        "###,
+        )?;
+
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          TO_CHAR(co.order_date, '%Y-%m') AS order_month,
+          TO_CHAR(co.order_date, '%Y-%m-%d') AS order_day,
+          COUNT(DISTINCT co.order_id) AS num_orders,
+          COUNT(ol.book_id) AS num_books,
+          SUM(ol.price) AS total_price,
+          SUM(COUNT(ol.book_id)) OVER (
+            PARTITION BY TO_CHAR(co.order_date, '%Y-%m')
+            ORDER BY
+              TO_CHAR(co.order_date, '%Y-%m-%d')
+          ) AS running_total_num_books,
+          LAG(COUNT(ol.book_id), 7) OVER (
+            ORDER BY
+              TO_CHAR(co.order_date, '%Y-%m-%d')
+          ) AS num_books_last_week
+        FROM
+          cust_order AS co
+          JOIN order_line AS ol USING(order_id)
+        GROUP BY
+          TO_CHAR(co.order_date, '%Y-%m'),
+          TO_CHAR(co.order_date, '%Y-%m-%d')
+        ORDER BY
+          order_day
+        "###);
+
+        // lag must be recognized as window function, even outside of group context
+        // rank must not have two OVER clauses
+        let query: Query = parse(
+            r###"
+        from daily_orders
+        derive [last_week: lag 7 num_orders]
+        group month ( | derive [total_month: sum num_orders])
+        "###,
+        )?;
+
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          daily_orders.*,
+          LAG(num_orders, 7) OVER () AS last_week,
+          SUM(num_orders) OVER (PARTITION BY month) AS total_month
+        FROM
+          daily_orders
+        "###);
+
+        // sort affects into groups
+        let query: Query = parse(
+            r###"
+        from daily_orders
+        sort day
+        group month ( | derive [total_month: rank])
+        derive [last_week: lag 7 num_orders]
+        "###,
+        )?;
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          daily_orders.*,
+          RANK() OVER (
+            PARTITION BY month
+            ORDER BY
+              day
+          ) AS total_month,
+          LAG(num_orders, 7) OVER (
+            ORDER BY
+              day
+          ) AS last_week
+        FROM
+          daily_orders
+        ORDER BY
+          day
+        "###);
+
+        // sort does not leak out of groups
+        let query: Query = parse(
+            r###"
+        from daily_orders
+        sort day
+        group month ( | sort num_orders |  derive [rank])
+        derive [num_orders_last_week: lag 7 num_orders]
+        "###,
+        )?;
+        assert_display_snapshot!((resolve_and_translate(query)?), @r###"
+        SELECT
+          daily_orders.*,
+          RANK() OVER (
+            PARTITION BY month
+            ORDER BY
+              num_orders
+          ) AS rank,
+          LAG(num_orders, 7) OVER (
+            ORDER BY
+              day
+          ) AS num_orders_last_week
+        FROM
+          daily_orders
+        ORDER BY
+          day
+        "###);
+
+        Ok(())
+    }
 }
diff --git a/prql-compiler/src/sql/un_group.rs b/prql-compiler/src/sql/un_group.rs
index e512d863..3d585fbd 100644
--- a/prql-compiler/src/sql/un_group.rs
+++ b/prql-compiler/src/sql/un_group.rs
@@ -21,7 +21,13 @@ impl AstFold for UnGrouper {
 
                     let pipeline = self.fold_nodes(pipeline.functions)?;
 
-                    res.extend(pipeline);
+                    res.extend(pipeline.into_iter().filter(|x| {
+                        // remove all sorts
+                        x.item
+                            .as_transform()
+                            .map(|t| !matches!(t, Transform::Sort(_)))
+                            .unwrap_or(true)
+                    }));
                 }
                 _ => {
                     res.push(self.fold_node(node)?);
diff --git a/reference/comparison-table.css b/reference/comparison-table.css
index 2e0bdf39..91c5657d 100644
--- a/reference/comparison-table.css
+++ b/reference/comparison-table.css
@@ -1,44 +1,32 @@
 /* Styling for Tables */
 
-/* Can't seem to manage to get the word breaks working well, even with:
-word-break: break-all; overflow-wrap: anywhere; word-wrap: anywhere;
-white-space: pre-wrap
-
-Would be good to fix this, or to make the page wider.
-
-*/
-
 .comparison {
     display: flex;
-    justify-content: center;
+    flex-wrap: wrap;
+    gap: 2rem;
     width: 100%;
+    padding-bottom: 3rem;
 }
-
-.comparison table {
-    width: 100%;
-    table-layout: fixed;
-}
-
-.comparison tr {
-    text-align: left;
-    vertical-align: top;
-    height: fit-content;
+@media (min-width: 1400px)  {
+    .comparison {
+        flex-wrap: nowrap;
+        justify-content: center;
+    }
 }
-
-.comparison th {
-    width: 50%;
+.comparison>div {
+    flex: 1 0 0;
+    display: flex;
+    flex-direction: column;
+    padding-bottom: 1rem;
 }
-
-.comparison td {
-    width: 50%;
-    padding: 0;
+.comparison h4 {
+    margin: 0;
 }
-
 .comparison pre {
     margin: 0;
+    flex-grow: 1;
 }
-
-/* Required to give enough space for the tables, up from 750px */
-.content main {
-    max-width: 1000px;
+.comparison code {
+    padding: 2rem;
+    min-height: calc(100% - 4rem);
 }
diff --git a/reference/mdbook-prql/src/main.rs b/reference/mdbook-prql/src/main.rs
index e826d895..25548876 100644
--- a/reference/mdbook-prql/src/main.rs
+++ b/reference/mdbook-prql/src/main.rs
@@ -130,28 +130,25 @@ fn table_of_comparison(prql: &str, sql: &str) -> String {
     format!(
         r#"
 <div class="comparison">
-<table>
-<tr>
-<th>PRQL</th><th>SQL</th>
-</tr>
 
-<tr>
-<td>
+<div>
+<h4>PRQL</h4>
 
 ```prql
 {prql}
 ```
 
-</td>
-<td>
+</div>
+
+<div>
+<h4>SQL</h4>
 
 ```sql
 {sql}
 ```
 
-</td>
-</tr>
-</table>
+</div>
+
 </div>
 "#,
         prql = prql.trim(),
diff --git a/reference/src/SUMMARY.md b/reference/src/SUMMARY.md
index 1ac2c393..8c0cf4c6 100644
--- a/reference/src/SUMMARY.md
+++ b/reference/src/SUMMARY.md
@@ -4,17 +4,28 @@
 - [Queries](./queries/README.md)
   - [Pipelines](./queries/pipelines.md)
 - [Language features](./language-features/README.md)
-
+  - [Strings](./language-features/strings.md)
   - [F-Strings](./language-features/f-strings.md)
+  - [S-Strings](./language-features/s-strings.md)
   - [Ranges](./language-features/ranges.md)
     <!-- - - Dates — `"2021-01-01"` -> `@2021-01-01`? And `DATE_TRUNC(foo_date, YEAR)` -> `foo_date.year`? Or -> `foo_date | as year`? Or `foo_date | to year`? -->
   - [Dates](./language-features/dates.md)
     <!--   - Regex — `REGEX_MATCH(foo, "\\w{3}")` -> `foo ~ r"\w{3}"`? Or -> `regex foo r"\w{3}"`? -->
   - [Regex]()
-  - [S-Strings](./language-features/s-strings.md)
 
 - [Transforms](./transforms.md)
 - [Syntax](./syntax.md)
 - [Functions](./functions.md)
 - [Stdlib](./stdlib.md)
 - [Live Editor](./editor.md)
+
+- [Examples](./examples/README.md)
+  - [Variables](./examples/variables.md)
+  - [List equivalence](./examples/list-equivalence.md)
+  - [Functions](./examples/functions.md)
+  - [CTE (intermediate tables)](./examples/cte.md)
+  - [Sql parameters](./examples/sql-parameters.md)
+  - [Employees 0](./examples/employees-0.md)
+  - [Employees 1](./examples/employees-1.md)
+  - [Employees 2](./examples/employees-2.md)
+  - [Employees 3](./examples/employees-3.md)
diff --git a/reference/src/examples/cte.md b/reference/src/examples/cte.md
index e2b4f402..65e9beb2 100644
--- a/reference/src/examples/cte.md
+++ b/reference/src/examples/cte.md
@@ -16,31 +16,3 @@ from newest_employees
 join average_salaries [country]
 select [name, salary, average_country_salary]
 ```
-
-```sql
-WITH newest_employees AS (
-  SELECT
-    employees.*
-  FROM
-    employees
-  ORDER BY
-    employees.tenure
-  LIMIT
-    50
-), average_salaries AS (
-  SELECT
-    country,
-    AVG(salary) AS average_country_salary
-  FROM
-    salaries
-  GROUP BY
-    country
-)
-SELECT
-  name,
-  salary,
-  average_country_salary
-FROM
-  newest_employees
-  JOIN average_salaries USING(country)
-```
diff --git a/reference/src/examples/employees-0.md b/reference/src/examples/employees-0.md
index 73c1f833..51504113 100644
--- a/reference/src/examples/employees-0.md
+++ b/reference/src/examples/employees-0.md
@@ -21,35 +21,3 @@ group [dept_emp.dept_no, t.title] (
 join departments [dept_no]
 select [dept_name, title, avg_salary]
 ```
-
-```sql
-WITH table_0 AS (
-  SELECT
-    emp_no,
-    AVG(salary) AS emp_salary
-  FROM
-    salaries
-  GROUP BY
-    emp_no
-),
-table_1 AS (
-  SELECT
-    dept_emp.dept_no,
-    t.title,
-    AVG(emp_salary) AS avg_salary
-  FROM
-    table_0
-    JOIN titles AS t USING(emp_no)
-    LEFT JOIN dept_emp USING(emp_no)
-  GROUP BY
-    dept_emp.dept_no,
-    t.title
-)
-SELECT
-  dept_name,
-  title,
-  avg_salary
-FROM
-  table_1
-  JOIN departments USING(dept_no)
-```
diff --git a/reference/src/examples/employees-1.md b/reference/src/examples/employees-1.md
index 123b9544..5273bed1 100644
--- a/reference/src/examples/employees-1.md
+++ b/reference/src/examples/employees-1.md
@@ -22,39 +22,3 @@ group [de.dept_no, gender] (
 join departments [dept_no]
 select [dept_name, gender, salary_avg, salary_sd]
 ```
-
-```sql
-WITH table_0 AS (
-  SELECT
-    emp_no,
-    gender,
-    AVG(salary) AS emp_salary
-  FROM
-    employees
-    JOIN salaries USING(emp_no)
-  GROUP BY
-    emp_no,
-    gender
-),
-table_1 AS (
-  SELECT
-    de.dept_no,
-    gender,
-    AVG(emp_salary) AS salary_avg,
-    STDDEV(emp_salary) AS salary_sd
-  FROM
-    table_0
-    LEFT JOIN dept_emp AS de USING(emp_no)
-  GROUP BY
-    de.dept_no,
-    gender
-)
-SELECT
-  dept_name,
-  gender,
-  salary_avg,
-  salary_sd
-FROM
-  table_1
-  JOIN departments USING(dept_no)
-```
diff --git a/reference/src/examples/employees-2.md b/reference/src/examples/employees-2.md
index 9dbb866a..12b8b313 100644
--- a/reference/src/examples/employees-2.md
+++ b/reference/src/examples/employees-2.md
@@ -28,42 +28,3 @@ join managers:employees [emp_no]
 derive [mng_name: s"managers.first_name || ' ' || managers.last_name"]
 select [mng_name, managers.gender, salary_avg, salary_sd]
 ```
-
-```sql
-WITH table_0 AS (
-  SELECT
-    emp_no,
-    gender,
-    AVG(salary) AS emp_salary
-  FROM
-    employees
-    JOIN salaries USING(emp_no)
-  GROUP BY
-    emp_no,
-    gender
-),
-table_1 AS (
-  SELECT
-    dm.emp_no,
-    gender,
-    AVG(emp_salary) AS salary_avg,
-    STDDEV(emp_salary) AS salary_sd,
-    dm.emp_no AS mng_no
-  FROM
-    table_0
-    JOIN dept_emp AS de USING(emp_no)
-    JOIN dept_manager AS dm ON dm.dept_no = de.dept_no
-    AND (de.from_date, de.to_date) OVERLAPS (dm.from_date, dm.to_date)
-  GROUP BY
-    dm.emp_no,
-    gender
-)
-SELECT
-  managers.first_name || ' ' || managers.last_name AS mng_name,
-  managers.gender,
-  salary_avg,
-  salary_sd
-FROM
-  table_1
-  JOIN employees AS managers USING(emp_no)
-```
diff --git a/reference/src/examples/employees-3.md b/reference/src/examples/employees-3.md
index 4b7b83fe..a86ad26f 100644
--- a/reference/src/examples/employees-3.md
+++ b/reference/src/examples/employees-3.md
@@ -3,38 +3,15 @@ Task:
 > Find distributions of titles, salaries and genders over different departments.
 
 ```prql
-from dept_emp
-join salaries side:left [
-  (salaries.emp_no = dept_emp.emp_no) and s"(salaries.from_date, salaries.to_date) OVERLAPS (dept_emp.from_date, dept_emp.to_date)"]
-group [dept_emp.emp_no, dept_emp.dept_no] (
-  aggregate salary: (average salaries.salary)
+from de:dept_emp
+join s:salaries side:left [
+  (s.emp_no = de.emp_no),
+  s"({s.from_date}, {s.to_date}) OVERLAPS ({de.from_date}, {de.to_date})"
+]
+group [de.emp_no, de.dept_no] (
+  aggregate salary: (average s.salary)
 )
 join employees [emp_no]
 join titles [emp_no]
 select [dept_no, salary, employees.gender, titles.title]
 ```
-
-```sql
-WITH table_0 AS (
-  SELECT
-    dept_emp.emp_no,
-    dept_emp.dept_no,
-    AVG(salaries.salary) AS salary
-  FROM
-    dept_emp
-    LEFT JOIN salaries ON salaries.emp_no = dept_emp.emp_no
-    AND (salaries.from_date, salaries.to_date) OVERLAPS (dept_emp.from_date, dept_emp.to_date)
-  GROUP BY
-    dept_emp.emp_no,
-    dept_emp.dept_no
-)
-SELECT
-  dept_no,
-  salary,
-  employees.gender,
-  titles.title
-FROM
-  table_0
-  JOIN employees USING(emp_no)
-  JOIN titles USING(emp_no)
-```
diff --git a/reference/src/examples/functions.md b/reference/src/examples/functions.md
index b4c6a906..8cd57433 100644
--- a/reference/src/examples/functions.md
+++ b/reference/src/examples/functions.md
@@ -1,5 +1,5 @@
-(This mostly works, apart from window clauses, and the version & db args on the
-first line)
+This mostly works, apart from window clauses, and the version & db args on the
+first line.
 
 ```prql_no_test
 prql version:0.1 db:snowflake                         # Version number & database name.
@@ -31,23 +31,6 @@ select [
 ]
 ```
 
-```sql
-SELECT
-  date,
-  -- Can't use a `WHERE` clause, as it would affect the row that the `LAG` function referenced.
-  IF(is_valid_price, price_adjusted / LAG(price_adjusted, 1) OVER
-    (PARTITION BY sec_id ORDER BY date) - 1 + dividend_return, NULL) AS return_total,
-  IF(is_valid_price, price_adjusted_usd / LAG(price_adjusted_usd, 1) OVER
-    (PARTITION BY sec_id ORDER BY date) - 1 + dividend_return, NULL) AS return_usd,
-  IF(is_valid_price, price_adjusted / LAG(price_adjusted, 1) OVER
-    (PARTITION BY sec_id ORDER BY date) - 1 + dividend_return, NULL)
-    - interest_rate / 252 AS return_excess,
-  IF(is_valid_price, price_adjusted_usd / LAG(price_adjusted_usd, 1) OVER
-    (PARTITION BY sec_id ORDER BY date) - 1 + dividend_return, NULL)
-    - interest_rate / 252 AS return_usd_excess
-FROM prices
-```
-
 ## Notes
 
 - The SQL can be a bit better, by pulling the window clause out.
diff --git a/reference/src/examples/list-equivalence.md b/reference/src/examples/list-equivalence.md
index 51222c36..431bde5a 100644
--- a/reference/src/examples/list-equivalence.md
+++ b/reference/src/examples/list-equivalence.md
@@ -1,25 +1,16 @@
+## Single item is coerced into a list
+
 ```prql
 from employees
 select salary
 ```
 
-```sql
-SELECT salary FROM employees
-```
-
----
-
+Same as above but with `salary` in a list:
 ```prql
-# Same as above but with `salary` in a list
 from employees
 select [salary]
 ```
-
-```sql
-SELECT salary FROM employees
-```
-
----
+## Multiple items
 
 ```prql
 from employees
@@ -29,27 +20,9 @@ derive [
 ]
 ```
 
-```sql
-SELECT TOP 20
-    *,
-    salary + payroll_tax AS gross_salary,
-    salary + payroll_tax + benefits_cost AS gross_cost
-FROM employees
-```
-
----
-
+Same as above but split into two lines:
 ```prql
-# Same as above but split into two lines
 from employees
 derive gross_salary: salary + payroll_tax
 derive gross_cost: gross_salary + benefits_cost
 ```
-
-```sql
-SELECT TOP 20
-    *,
-    salary + payroll_tax AS gross_salary,
-    salary + payroll_tax + benefits_cost AS gross_cost
-FROM employees
-```
diff --git a/reference/src/examples/sql-parameters.md b/reference/src/examples/sql-parameters.md
index 41222118..9aa6a37c 100644
--- a/reference/src/examples/sql-parameters.md
+++ b/reference/src/examples/sql-parameters.md
@@ -2,12 +2,3 @@
 from mytable
 filter id = $1
 ```
-
-```sql
-SELECT
-  mytable.*
-FROM
-  mytable
-WHERE
-  id = $1
-```
diff --git a/reference/src/examples/variables.md b/reference/src/examples/variables.md
index 262cbd1f..a34fd6af 100644
--- a/reference/src/examples/variables.md
+++ b/reference/src/examples/variables.md
@@ -21,70 +21,3 @@ sort sum_gross_cost
 filter ct > 200
 take 20
 ```
-
-```sql
-SELECT
-  title,
-  country,
-  AVG(salary),
-  SUM(salary),
-  AVG(salary + payroll_tax),
-  SUM(salary + payroll_tax),
-  AVG(salary + payroll_tax + benefits_cost),
-  SUM(salary + payroll_tax + benefits_cost) AS sum_gross_cost,
-  COUNT(*) AS ct
-FROM
-  employees
-WHERE
-  country = 'USA'
-  AND salary + payroll_tax + benefits_cost > 0
-GROUP BY
-  title,
-  country
-HAVING
-  COUNT(*) > 200
-ORDER BY
-  sum_gross_cost
-LIMIT
-  20
-```
-
-```prql
-from employees
-group [emp_no] (
-  aggregate [
-    emp_salary: average salary        # avg_salary should resolve to "AVG(salary)" (from stdlib)
-  ]
-)
-join titles [emp_no]
-group [title] (
-  aggregate [
-    avg_salary: average emp_salary
-  ]
-)
-select salary_k: avg_salary / 1000    # avg_salary should resolve to "AVG(emp_salary)"
-take 10                               # induces new SELECT
-derive salary: salary_k * 1000        # salary_k should not resolve to "avg_salary / 1000"
-```
-
-```sql
-WITH table_0 AS (
-  SELECT
-    emp_no,
-    AVG(salary) AS emp_salary
-  FROM
-    employees
-  GROUP BY
-    emp_no
-)
-SELECT
-  AVG(emp_salary) / 1000 AS salary_k,
-  AVG(emp_salary) / 1000 * 1000 AS salary
-FROM
-  table_0
-  JOIN titles USING(emp_no)
-GROUP BY
-  title
-LIMIT
-  10
-```
diff --git a/reference/src/introduction.md b/reference/src/introduction.md
index ec315e25..f2fd34f3 100644
--- a/reference/src/introduction.md
+++ b/reference/src/introduction.md
@@ -15,14 +15,14 @@ Let's get started with an example:
 
 ```prql
 from employees
-filter country_code = "USA"                  # Each line transforms the previous result.
-derive [                                     # This adds columns / variables.
+filter country_code = "USA"    # Each line transforms the previous result.
+derive [                       # This adds columns / variables.
   gross_salary: salary + payroll_tax,
-  gross_cost:  gross_salary + benefits_cost  # Variables can use other variables.
+  gross_cost: gross_salary + benefits_cost  # Variables can use other variables.
 ]
 filter gross_cost > 0
-group [title, country_code] (                # For each group use a nested pipeline
-  aggregate [                                # Aggregate each group to a single row
+group [title, country_code] (  # For each group use a nested pipeline
+  aggregate [                  # Aggregate each group to a single row
     average salary,
     average gross_salary,
     sum salary,
@@ -38,7 +38,7 @@ take 20
 join countries side:left [country_code]
 derive [
   always_true: true,
-  db_version: s"version()",                  # An S-string, which transpiles directly into SQL
+  db_version: s"version()",    # An S-string, which transpiles directly into SQL
 ]
 ```
 
diff --git a/reference/src/language-features/strings.md b/reference/src/language-features/strings.md
new file mode 100644
index 00000000..24a4783f
--- /dev/null
+++ b/reference/src/language-features/strings.md
@@ -0,0 +1,28 @@
+# Strings
+
+Strings in PRQL can use either single or double quotes:
+
+```prql
+derive x: "hello world"
+```
+
+```prql
+derive x: 'hello world'
+```
+
+To quote a string containing quotes, either use the "other" type of quote, or
+use three-or-more quotes, and close with the same number.
+
+```prql
+derive x: '"hello world"'
+```
+
+```prql
+derive x: """I said "hello world"!"""
+```
+
+Currently PRQL does not adjust escape characters during the compilation process.
+
+Currently PRQL allows multiline strings with either a single character or
+multiple character quotes. This may change for strings using a single character
+quote in future versions.
diff --git a/reference/src/syntax.md b/reference/src/syntax.md
index 04886abb..78cbdac2 100644
--- a/reference/src/syntax.md
+++ b/reference/src/syntax.md
@@ -18,20 +18,20 @@
 
 - A line-break generally creates a pipelined transformation. For example:
 
-  ```prql
-  from tbl
-  select [
-    col1,
-    col2,
-  ]
-  filter col1 = col2
-  ```
+```prql
+from tbl
+select [
+  col1,
+  col2,
+]
+filter col1 = col2
+```
 
   ...is equivalent to:
 
-  ```prql
-  from tbl | select [col1, col2] | filter col1 = col2
-  ```
+```prql
+from tbl | select [col1, col2] | filter col1 = col2
+```
 
 - A line-break doesn't created a pipeline in a few cases:
   - Within a list (e.g. the `select` example above).
@@ -40,7 +40,7 @@
 
 ### CTEs
 
-- See [CTE Example](examples/cte-1.md).
+- See [CTE Example](examples/cte.md).
 - This is no longer point-free, but that's a feature rather than a requirement.
   The alternative is subqueries, which are fine at small scale, but become
   difficult to digest as complexity increases.


Test patch:
diff --git a/.github/workflows/test-all.yaml b/.github/workflows/test-all.yaml
index 1e15adf7..1894da12 100644
--- a/.github/workflows/test-all.yaml
+++ b/.github/workflows/test-all.yaml
@@ -14,45 +14,19 @@ concurrency:
   cancel-in-progress: true
 
 jobs:
-  # Three identical jobs apart from the OS. Ideally we'd be able to parameterize
-  # these, but:
-  # - We want to call to be able to call this workflow from `pull-request`, so
-  #   we can't call another workflow from here, given there's a max depth of 2.
-  # - We can't seem to specify the OS of an action.
-  # If we can compress this into something parameterized, that would be great.
-  test-linux:
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        target: ["", "wasm32-unknown-unknown"]
-    steps:
-      - name: 📂 Checkout code
-        uses: actions/checkout@v2
-      - uses: ./.github/actions/cargo-test
-        with:
-          target: ${{ matrix.target }}
-  test-windows:
-    runs-on: windows-latest
-    strategy:
-      matrix:
-        target: ["", "wasm32-unknown-unknown"]
-    steps:
-      - name: 📂 Checkout code
-        uses: actions/checkout@v2
-      - uses: ./.github/actions/cargo-test
-        with:
-          target: ${{ matrix.target }}
-  test-mac:
-    runs-on: macos-latest
+  test:
+    runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         target: ["", "wasm32-unknown-unknown"]
+        os: [windows-latest, macos-latest, ubuntu-latest]
     steps:
       - name: 📂 Checkout code
         uses: actions/checkout@v2
       - uses: ./.github/actions/cargo-test
         with:
           target: ${{ matrix.target }}
+
   code-cov:
     runs-on: ubuntu-latest
     # Currently this runs `cargo clean` which makes the run taking 4 min. TODO:
diff --git a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree-4.snap b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree-4.snap
index 99ae899b..e81aa39f 100644
--- a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree-4.snap
+++ b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree-4.snap
@@ -107,7 +107,7 @@ expression: "parse_tree_of_str(r#\"    filter country = \"USA\"\"#, Rule::pipeli
                                                         },
                                                         inner: [
                                                             Pair {
-                                                                rule: string_literal,
+                                                                rule: string,
                                                                 span: Span {
                                                                     str: "\"USA\"",
                                                                     start: 21,
diff --git a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree.snap b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree.snap
index 6af3ce1f..6a904f86 100644
--- a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree.snap
+++ b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_into_parse_tree.snap
@@ -74,7 +74,7 @@ expression: "parse_tree_of_str(r#\"country = \"USA\"\"#, Rule::expr)?"
                                 },
                                 inner: [
                                     Pair {
-                                        rule: string_literal,
+                                        rule: string,
                                         span: Span {
                                             str: "\"USA\"",
                                             start: 10,
diff --git a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-2.snap b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-2.snap
index e364e6aa..d87e6da7 100644
--- a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-2.snap
+++ b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-2.snap
@@ -1,6 +1,6 @@
 ---
 source: prql-compiler/src/parser.rs
-expression: "parse_tree_of_str(r#\"\n    from employees\n    filter country = \"USA\"\n    \"#.trim(),\n                  Rule::pipeline)?"
+expression: "parse_tree_of_str(r#\"\n    from employees\n    filter country = \"USA\"\n    \"#.trim(),\n        Rule::pipeline)?"
 ---
 [
     Pair {
@@ -185,7 +185,7 @@ expression: "parse_tree_of_str(r#\"\n    from employees\n    filter country = \"
                                                         },
                                                         inner: [
                                                             Pair {
-                                                                rule: string_literal,
+                                                                rule: string,
                                                                 span: Span {
                                                                     str: "\"USA\"",
                                                                     start: 36,
diff --git a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-3.snap b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-3.snap
index 94e22be9..0f7aa828 100644
--- a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-3.snap
+++ b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_pipeline_parse_tree-3.snap
@@ -1,6 +1,6 @@
 ---
 source: prql-compiler/src/parser.rs
-expression: "parse_tree_of_str(r#\"\nfrom employees\nfilter country = \"USA\"                           # Each line transforms the previous result.\nderive [                                         # This adds columns / variables.\n  gross_salary: salary + payroll_tax,\n  gross_cost  : gross_salary + benefits_cost    # Variables can use other variables.\n]\nfilter gross_cost > 0\ngroup [title, country] (\n    aggregate [                                  # `by` are the columns to group by.\n        average salary,                          # These are aggregation calcs run on each group.\n        sum     salary,\n        average gross_salary,\n        sum     gross_salary,\n        average gross_cost,\n        sum_gross_cost: sum gross_cost,\n        count: count,\n    ]\n)\nsort sum_gross_cost\nfilter count > 200\ntake 20\n    \"#.trim(),\n                  Rule::pipeline)?"
+expression: "parse_tree_of_str(r#\"\nfrom employees\nfilter country = \"USA\"                           # Each line transforms the previous result.\nderive [                                         # This adds columns / variables.\n  gross_salary: salary + payroll_tax,\n  gross_cost  : gross_salary + benefits_cost    # Variables can use other variables.\n]\nfilter gross_cost > 0\ngroup [title, country] (\n    aggregate [                                  # `by` are the columns to group by.\n        average salary,                          # These are aggregation calcs run on each group.\n        sum     salary,\n        average gross_salary,\n        sum     gross_salary,\n        average gross_cost,\n        sum_gross_cost: sum gross_cost,\n        count: count,\n    ]\n)\nsort sum_gross_cost\nfilter count > 200\ntake 20\n    \"#.trim(),\n        Rule::pipeline)?"
 ---
 [
     Pair {
@@ -185,7 +185,7 @@ expression: "parse_tree_of_str(r#\"\nfrom employees\nfilter country = \"USA\"
                                                         },
                                                         inner: [
                                                             Pair {
-                                                                rule: string_literal,
+                                                                rule: string,
                                                                 span: Span {
                                                                     str: "\"USA\"",
                                                                     start: 32,
diff --git a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_s_string.snap b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_s_string.snap
index aa60f4cd..0933a9e8 100644
--- a/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_s_string.snap
+++ b/prql-compiler/src/snapshots/prql_compiler__parser__test__parse_s_string.snap
@@ -44,7 +44,7 @@ expression: "parse_tree_of_str(r#\"s\"SUM({col})\"\"#, Rule::expr_call)?"
                                         },
                                         inner: [
                                             Pair {
-                                                rule: interpolate_string,
+                                                rule: interpolate_string_inner,
                                                 span: Span {
                                                     str: "SUM(",
                                                     start: 2,
@@ -102,7 +102,7 @@ expression: "parse_tree_of_str(r#\"s\"SUM({col})\"\"#, Rule::expr_call)?"
                                                 ],
                                             },
                                             Pair {
-                                                rule: interpolate_string,
+                                                rule: interpolate_string_inner,
                                                 span: Span {
                                                     str: ")",
                                                     start: 11,
diff --git a/prql-compiler/src/snapshots/prql_compiler__translator__test__prql_to_sql_2.snap b/prql-compiler/src/snapshots/prql_compiler__translator__test__prql_to_sql_2.snap
deleted file mode 100644
index 76578aea..00000000
--- a/prql-compiler/src/snapshots/prql_compiler__translator__test__prql_to_sql_2.snap
+++ /dev/null
@@ -1,28 +0,0 @@
----
-source: prql-compiler/src/translator.rs
-expression: sql
----
-SELECT
-  title,
-  country,
-  AVG(salary),
-  SUM(salary),
-  AVG(salary + payroll_tax),
-  SUM(salary + payroll_tax),
-  AVG(salary + payroll_tax + benefits_cost),
-  SUM(salary + payroll_tax + benefits_cost) AS sum_gross_cost,
-  COUNT(*) AS ct
-FROM
-  employees
-WHERE
-  country = 'USA'
-  AND salary + payroll_tax + benefits_cost > 0
-GROUP BY
-  title,
-  country
-HAVING
-  COUNT(*) > 200
-ORDER BY
-  sum_gross_cost
-LIMIT
-  20
diff --git a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_2.snap b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_2.snap
index 4d3f490a..e6e6790d 100644
--- a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_2.snap
+++ b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_2.snap
@@ -13,23 +13,6 @@ expression: mat
           - Ident: country
           - Raw: "="
           - String: USA
-- Transform:
-    Derive:
-      assigns:
-        - Expr:
-            - Ident: salary
-            - Raw: +
-            - Ident: payroll_tax
-        - Expr:
-            - Expr:
-                - Ident: salary
-                - Raw: +
-                - Ident: payroll_tax
-            - Raw: +
-            - Ident: benefits_cost
-      group: []
-      window: ~
-      sort: ~
 - Transform:
     Filter:
       - Expr:
@@ -104,26 +87,9 @@ expression: mat
                         - String: )
                     - SString:
                         - String: COUNT(*)
-                  group:
+                  by:
                     - Ident: title
                     - Ident: country
-                  window: ~
-                  sort: ~
-- Transform:
-    Sort:
-      - direction: Asc
-        column:
-          SString:
-            - String: SUM(
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: salary
-                      - Raw: +
-                      - Ident: payroll_tax
-                  - Raw: +
-                  - Ident: benefits_cost
-            - String: )
 - Transform:
     Filter:
       - Expr:
diff --git a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_3.snap b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_3.snap
index a7b82008..95658ea5 100644
--- a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_3.snap
+++ b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__materialize_3.snap
@@ -7,178 +7,4 @@ expression: mat
       name: prices
       alias: ~
       declared_at: 40
-- Transform:
-    Derive:
-      assigns:
-        - SString:
-            - String: "IF(is_valid_price, "
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: prices_adj
-                      - Raw: /
-                      - SString:
-                          - String: lag_day_todo(
-                          - Expr:
-                              Ident: prices_adj
-                          - String: )
-                  - Raw: "-"
-                  - Raw: "1"
-                  - Raw: +
-                  - Ident: div_ret
-            - String: ", NULL)"
-        - SString:
-            - String: "IF(is_valid_price, "
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: prices_usd
-                      - Raw: /
-                      - SString:
-                          - String: lag_day_todo(
-                          - Expr:
-                              Ident: prices_usd
-                          - String: )
-                  - Raw: "-"
-                  - Raw: "1"
-                  - Raw: +
-                  - Ident: div_ret
-            - String: ", NULL)"
-        - Expr:
-            - Expr:
-                - SString:
-                    - String: "IF(is_valid_price, "
-                    - Expr:
-                        Expr:
-                          - Expr:
-                              - Ident: prices_adj
-                              - Raw: /
-                              - SString:
-                                  - String: lag_day_todo(
-                                  - Expr:
-                                      Ident: prices_adj
-                                  - String: )
-                          - Raw: "-"
-                          - Raw: "1"
-                          - Raw: +
-                          - Ident: div_ret
-                    - String: ", NULL)"
-                - Raw: "-"
-                - Raw: "0.2"
-            - Raw: /
-            - Raw: "252"
-        - Expr:
-            - Expr:
-                - SString:
-                    - String: "IF(is_valid_price, "
-                    - Expr:
-                        Expr:
-                          - Expr:
-                              - Ident: prices_usd
-                              - Raw: /
-                              - SString:
-                                  - String: lag_day_todo(
-                                  - Expr:
-                                      Ident: prices_usd
-                                  - String: )
-                          - Raw: "-"
-                          - Raw: "1"
-                          - Raw: +
-                          - Ident: div_ret
-                    - String: ", NULL)"
-                - Raw: "-"
-                - Raw: "0.2"
-            - Raw: /
-            - Raw: "252"
-      group: []
-      window: ~
-      sort: ~
-- Transform:
-    Select:
-      assigns:
-        - Ident: date
-        - Ident: sec_id
-        - SString:
-            - String: "IF(is_valid_price, "
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: prices_adj
-                      - Raw: /
-                      - SString:
-                          - String: lag_day_todo(
-                          - Expr:
-                              Ident: prices_adj
-                          - String: )
-                  - Raw: "-"
-                  - Raw: "1"
-                  - Raw: +
-                  - Ident: div_ret
-            - String: ", NULL)"
-        - SString:
-            - String: "IF(is_valid_price, "
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: prices_usd
-                      - Raw: /
-                      - SString:
-                          - String: lag_day_todo(
-                          - Expr:
-                              Ident: prices_usd
-                          - String: )
-                  - Raw: "-"
-                  - Raw: "1"
-                  - Raw: +
-                  - Ident: div_ret
-            - String: ", NULL)"
-        - Expr:
-            - Expr:
-                - SString:
-                    - String: "IF(is_valid_price, "
-                    - Expr:
-                        Expr:
-                          - Expr:
-                              - Ident: prices_adj
-                              - Raw: /
-                              - SString:
-                                  - String: lag_day_todo(
-                                  - Expr:
-                                      Ident: prices_adj
-                                  - String: )
-                          - Raw: "-"
-                          - Raw: "1"
-                          - Raw: +
-                          - Ident: div_ret
-                    - String: ", NULL)"
-                - Raw: "-"
-                - Raw: "0.2"
-            - Raw: /
-            - Raw: "252"
-        - Expr:
-            - Expr:
-                - SString:
-                    - String: "IF(is_valid_price, "
-                    - Expr:
-                        Expr:
-                          - Expr:
-                              - Ident: prices_usd
-                              - Raw: /
-                              - SString:
-                                  - String: lag_day_todo(
-                                  - Expr:
-                                      Ident: prices_usd
-                                  - String: )
-                          - Raw: "-"
-                          - Raw: "1"
-                          - Raw: +
-                          - Ident: div_ret
-                    - String: ", NULL)"
-                - Raw: "-"
-                - Raw: "0.2"
-            - Raw: /
-            - Raw: "252"
-      group: []
-      window: ~
-      sort: ~
 
diff --git a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__replace_variables_2.snap b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__replace_variables_2.snap
index 281b651f..29b496b5 100644
--- a/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__replace_variables_2.snap
+++ b/prql-compiler/src/sql/snapshots/prql_compiler__sql__materializer__test__replace_variables_2.snap
@@ -13,23 +13,6 @@ expression: "&mat"
           - Ident: country
           - Raw: "="
           - String: USA
-- Transform:
-    Derive:
-      assigns:
-        - Expr:
-            - Ident: salary
-            - Raw: +
-            - Ident: payroll_tax
-        - Expr:
-            - Expr:
-                - Ident: salary
-                - Raw: +
-                - Ident: payroll_tax
-            - Raw: +
-            - Ident: benefits_cost
-      group: []
-      window: ~
-      sort: ~
 - Transform:
     Filter:
       - Expr:
@@ -104,26 +87,9 @@ expression: "&mat"
                         - String: )
                     - SString:
                         - String: COUNT(*)
-                  group:
+                  by:
                     - Ident: title
                     - Ident: country
-                  window: ~
-                  sort: ~
-- Transform:
-    Sort:
-      - direction: Asc
-        column:
-          SString:
-            - String: SUM(
-            - Expr:
-                Expr:
-                  - Expr:
-                      - Ident: salary
-                      - Raw: +
-                      - Ident: payroll_tax
-                  - Raw: +
-                  - Ident: benefits_cost
-            - String: )
 - Transform:
     Filter:
       - Expr:
diff --git a/reference/tests/examples/examples/employees-3-0.prql b/reference/tests/examples/examples/employees-3-0.prql
index 2cb9169e..3bd5e044 100644
--- a/reference/tests/examples/examples/employees-3-0.prql
+++ b/reference/tests/examples/examples/employees-3-0.prql
@@ -1,8 +1,10 @@
-from dept_emp
-join salaries side:left [
-  (salaries.emp_no = dept_emp.emp_no) and s"(salaries.from_date, salaries.to_date) OVERLAPS (dept_emp.from_date, dept_emp.to_date)"]
-group [dept_emp.emp_no, dept_emp.dept_no] (
-  aggregate salary: (average salaries.salary)
+from de:dept_emp
+join s:salaries side:left [
+  (s.emp_no = de.emp_no),
+  s"({s.from_date}, {s.to_date}) OVERLAPS ({de.from_date}, {de.to_date})"
+]
+group [de.emp_no, de.dept_no] (
+  aggregate salary: (average s.salary)
 )
 join employees [emp_no]
 join titles [emp_no]
diff --git a/reference/tests/examples/examples/list-equivalence-1.prql b/reference/tests/examples/examples/list-equivalence-1.prql
index e1e9c6c3..3b89f425 100644
--- a/reference/tests/examples/examples/list-equivalence-1.prql
+++ b/reference/tests/examples/examples/list-equivalence-1.prql
@@ -1,3 +1,2 @@
-# Same as above but with `salary` in a list
 from employees
 select [salary]
diff --git a/reference/tests/examples/examples/list-equivalence-3.prql b/reference/tests/examples/examples/list-equivalence-3.prql
index 77ba439f..bfb10143 100644
--- a/reference/tests/examples/examples/list-equivalence-3.prql
+++ b/reference/tests/examples/examples/list-equivalence-3.prql
@@ -1,4 +1,3 @@
-# Same as above but split into two lines
 from employees
 derive gross_salary: salary + payroll_tax
 derive gross_cost: gross_salary + benefits_cost
diff --git a/reference/tests/examples/introduction-0.prql b/reference/tests/examples/introduction-0.prql
index 0be58f27..cfe31212 100644
--- a/reference/tests/examples/introduction-0.prql
+++ b/reference/tests/examples/introduction-0.prql
@@ -1,12 +1,12 @@
 from employees
-filter country_code = "USA"                  # Each line transforms the previous result.
-derive [                                     # This adds columns / variables.
+filter country_code = "USA"    # Each line transforms the previous result.
+derive [                       # This adds columns / variables.
   gross_salary: salary + payroll_tax,
-  gross_cost:  gross_salary + benefits_cost  # Variables can use other variables.
+  gross_cost: gross_salary + benefits_cost  # Variables can use other variables.
 ]
 filter gross_cost > 0
-group [title, country_code] (                # For each group use a nested pipeline
-  aggregate [                                # Aggregate each group to a single row
+group [title, country_code] (  # For each group use a nested pipeline
+  aggregate [                  # Aggregate each group to a single row
     average salary,
     average gross_salary,
     sum salary,
@@ -22,5 +22,5 @@ take 20
 join countries side:left [country_code]
 derive [
   always_true: true,
-  db_version: s"version()",                  # An S-string, which transpiles directly into SQL
+  db_version: s"version()",    # An S-string, which transpiles directly into SQL
 ]
diff --git a/reference/tests/examples/language-features/strings-0.prql b/reference/tests/examples/language-features/strings-0.prql
new file mode 100644
index 00000000..089c7d19
--- /dev/null
+++ b/reference/tests/examples/language-features/strings-0.prql
@@ -0,0 +1,1 @@
+derive x: "hello world"
diff --git a/reference/tests/examples/language-features/strings-1.prql b/reference/tests/examples/language-features/strings-1.prql
new file mode 100644
index 00000000..7841f16c
--- /dev/null
+++ b/reference/tests/examples/language-features/strings-1.prql
@@ -0,0 +1,1 @@
+derive x: 'hello world'
diff --git a/reference/tests/examples/language-features/strings-2.prql b/reference/tests/examples/language-features/strings-2.prql
new file mode 100644
index 00000000..8cc42365
--- /dev/null
+++ b/reference/tests/examples/language-features/strings-2.prql
@@ -0,0 +1,1 @@
+derive x: '"hello world"'
diff --git a/reference/tests/examples/language-features/strings-3.prql b/reference/tests/examples/language-features/strings-3.prql
new file mode 100644
index 00000000..24f3351a
--- /dev/null
+++ b/reference/tests/examples/language-features/strings-3.prql
@@ -0,0 +1,1 @@
+derive x: """I said "hello world"!"""
diff --git a/reference/tests/examples/syntax-0.prql b/reference/tests/examples/syntax-0.prql
index f9a482cd..1acb8905 100644
--- a/reference/tests/examples/syntax-0.prql
+++ b/reference/tests/examples/syntax-0.prql
@@ -1,1 +1,6 @@
 from tbl
+select [
+  col1,
+  col2,
+]
+filter col1 = col2
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@employees-3-0.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@employees-3-0.prql.snap
index 4ba3342e..351351d1 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@employees-3-0.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@employees-3-0.prql.snap
@@ -5,16 +5,15 @@ input_file: reference/tests/examples/examples/employees-3-0.prql
 ---
 WITH table_0 AS (
   SELECT
-    dept_emp.emp_no,
-    dept_emp.dept_no,
-    AVG(salaries.salary) AS salary
+    de.emp_no,
+    de.dept_no,
+    AVG(s.salary) AS salary
   FROM
-    dept_emp
-    LEFT JOIN salaries ON salaries.emp_no = dept_emp.emp_no
-    AND (salaries.from_date, salaries.to_date) OVERLAPS (dept_emp.from_date, dept_emp.to_date)
+    dept_emp AS de
+    LEFT JOIN salaries AS s ON s.emp_no = de.emp_no (s.from_date, s.to_date) OVERLAPS (de.from_date, de.to_date)
   GROUP BY
-    dept_emp.emp_no,
-    dept_emp.dept_no
+    de.emp_no,
+    de.dept_no
 )
 SELECT
   table_0.dept_no,
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-2.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-2.prql.snap
index 030834db..266d4531 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-2.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-2.prql.snap
@@ -6,6 +6,6 @@ input_file: reference/tests/examples/examples/list-equivalence-2.prql
 SELECT
   employees.*,
   salary + payroll_tax AS gross_salary,
-  gross_salary + benefits_cost AS gross_cost
+  salary + payroll_tax + benefits_cost AS gross_cost
 FROM
   employees
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-3.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-3.prql.snap
index b301494f..c57bddbd 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-3.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@list-equivalence-3.prql.snap
@@ -6,6 +6,6 @@ input_file: reference/tests/examples/examples/list-equivalence-3.prql
 SELECT
   employees.*,
   salary + payroll_tax AS gross_salary,
-  gross_salary + benefits_cost AS gross_cost
+  salary + payroll_tax + benefits_cost AS gross_cost
 FROM
   employees
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@strings-0.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@strings-0.prql.snap
new file mode 100644
index 00000000..38282067
--- /dev/null
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@strings-0.prql.snap
@@ -0,0 +1,7 @@
+---
+source: reference/tests/snapshot.rs
+expression: sql
+input_file: reference/tests/examples/language-features/strings-0.prql
+---
+SELECT
+  'hello world' AS x
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@strings-1.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@strings-1.prql.snap
new file mode 100644
index 00000000..4be09f0c
--- /dev/null
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@strings-1.prql.snap
@@ -0,0 +1,7 @@
+---
+source: reference/tests/snapshot.rs
+expression: sql
+input_file: reference/tests/examples/language-features/strings-1.prql
+---
+SELECT
+  'hello world' AS x
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@strings-2.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@strings-2.prql.snap
new file mode 100644
index 00000000..370820cf
--- /dev/null
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@strings-2.prql.snap
@@ -0,0 +1,7 @@
+---
+source: reference/tests/snapshot.rs
+expression: sql
+input_file: reference/tests/examples/language-features/strings-2.prql
+---
+SELECT
+  '"hello world"' AS x
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@strings-3.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@strings-3.prql.snap
new file mode 100644
index 00000000..bdc5d4ff
--- /dev/null
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@strings-3.prql.snap
@@ -0,0 +1,7 @@
+---
+source: reference/tests/snapshot.rs
+expression: sql
+input_file: reference/tests/examples/language-features/strings-3.prql
+---
+SELECT
+  'I said "hello world"!' AS x
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@syntax-0.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@syntax-0.prql.snap
index 8201a75c..71f69458 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@syntax-0.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@syntax-0.prql.snap
@@ -4,6 +4,9 @@ expression: sql
 input_file: reference/tests/examples/syntax-0.prql
 ---
 SELECT
-  tbl.*
+  col1,
+  col2
 FROM
   tbl
+WHERE
+  col1 = col2
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@transforms-3.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@transforms-3.prql.snap
index b68c34db..4102297c 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@transforms-3.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@transforms-3.prql.snap
@@ -6,6 +6,6 @@ input_file: reference/tests/examples/transforms-3.prql
 SELECT
   employees.*,
   salary + payroll_tax AS gross_salary,
-  gross_salary + benefits_cost AS gross_cost
+  salary + payroll_tax + benefits_cost AS gross_cost
 FROM
   employees
diff --git a/reference/tests/snapshots/snapshot__run_reference_examples@variables-1.prql.snap b/reference/tests/snapshots/snapshot__run_reference_examples@variables-1.prql.snap
index 18119087..2b627a2f 100644
--- a/reference/tests/snapshots/snapshot__run_reference_examples@variables-1.prql.snap
+++ b/reference/tests/snapshots/snapshot__run_reference_examples@variables-1.prql.snap
@@ -14,7 +14,7 @@ WITH table_0 AS (
 )
 SELECT
   AVG(table_0.emp_salary) / 1000 AS salary_k,
-  salary_k * 1000 AS salary
+  AVG(table_0.emp_salary) / 1000 * 1000 AS salary
 FROM
   table_0
   JOIN titles USING(emp_no)