Repository: typedb/typeql (language: python)

Issue:
String escaping behaviour creates invalid queries and limits possible strings
## Description

Strings are not escaped when converting Java TypeQL to a query string, despite requiring escape characters in order for the query string to be valid.

It also appears that the required escape characters are inserted, rather than being removed during parsing of the query, and that this was an oversight during a bugfix for asymmetrical escaping behaviour in an older version of TypeQLpre 1.0).

The behaviour is not standard, can cause users to accidentally break their queries (when programmatically generating them) and prevents users from inserting any string.

## Environment

1. OS (where TypeQL server runs): any
2. TypeQL version (and platform): any
3. TypeQL client: any (most notable in client-java)
4. Other environment details:

## Reproducible Steps

Steps to create the smallest reproducible scenario:
1.
```
// Gives us an invalid TypeQL query
System.out.println(TypeQL.insert(TypeQL.var("x").isa("person").has("name", "b\"o\"b")).toString());
```

## Expected Output

```
insert $x isa person, has name "b\"o\"b";
```

## Actual Output

```
insert $x isa person, has name "b"o"b";
```



Discussion / hints:
Stashed PR here: https://github.com/vaticle/typeql/pull/224
Further considerations are in the comment there

Fix patch:
diff --git a/dependencies/maven/artifacts.snapshot b/dependencies/maven/artifacts.snapshot
index 74d24c0c8..bda1f60a1 100644
--- a/dependencies/maven/artifacts.snapshot
+++ b/dependencies/maven/artifacts.snapshot
@@ -1,9 +1,9 @@
 @maven//:com_eclipsesource_minimal_json_minimal_json_0_9_5
 @maven//:com_electronwill_night_config_core_3_6_5
 @maven//:com_electronwill_night_config_toml_3_6_5
-@maven//:com_fasterxml_jackson_core_jackson_annotations_2_10_1
-@maven//:com_fasterxml_jackson_core_jackson_core_2_10_1
-@maven//:com_fasterxml_jackson_core_jackson_databind_2_10_1
+@maven//:com_fasterxml_jackson_core_jackson_annotations_2_16_0
+@maven//:com_fasterxml_jackson_core_jackson_core_2_16_0
+@maven//:com_fasterxml_jackson_core_jackson_databind_2_16_0
 @maven//:com_google_code_findbugs_jsr305_3_0_2
 @maven//:com_google_errorprone_error_prone_annotations_2_3_4
 @maven//:com_google_guava_failureaccess_1_0_1
@@ -11,6 +11,7 @@
 @maven//:com_google_guava_listenablefuture_9999_0_empty_to_avoid_conflict_with_guava
 @maven//:com_google_http_client_google_http_client_1_34_2
 @maven//:com_google_j2objc_j2objc_annotations_1_3
+@maven//:com_vdurmont_semver4j_3_1_0
 @maven//:commons_codec_commons_codec_1_13
 @maven//:commons_io_commons_io_2_3
 @maven//:commons_logging_commons_logging_1_2
@@ -42,5 +43,5 @@
 @maven//:org_jsoup_jsoup_1_16_1
 @maven//:org_kohsuke_github_api_1_101
 @maven//:org_slf4j_slf4j_api_2_0_0
-@maven//:org_yaml_snakeyaml_1_25
+@maven//:org_yaml_snakeyaml_2_2
 @maven//:org_zeroturnaround_zt_exec_1_10
diff --git a/dependencies/vaticle/repositories.bzl b/dependencies/vaticle/repositories.bzl
index a372bd557..6f8a4ad22 100644
--- a/dependencies/vaticle/repositories.bzl
+++ b/dependencies/vaticle/repositories.bzl
@@ -7,8 +7,8 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
 def vaticle_dependencies():
     git_repository(
         name = "vaticle_dependencies",
-        remote = "https://github.com/dmitrii-ubskii/vaticle-dependencies",
-        commit = "b13ccddfddcac1d56a029d98e3893f74b267c5ae", # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_dependencies
+        remote = "https://github.com/vaticle/dependencies",
+        commit = "78d937a49135453965ddd68c51de7f90b8aeba15",  # sync-marker: do not remove this comment, this is used for sync-dependencies by @vaticle_dependencies
     )
 
 def vaticle_typedb_behaviour():
diff --git a/rust/BUILD b/rust/BUILD
index 66046427f..1bf21c819 100644
--- a/rust/BUILD
+++ b/rust/BUILD
@@ -88,14 +88,7 @@ checkstyle_test(
     name = "checkstyle",
     include = glob([
         "*",
-        "builder/**",
-        "common/**",
-        "parser/**",
-        "pattern/**",
-        "query/**",
-        "tests/**",
-        "util/**",
-        "variable/**",
+        "*/**",
       ], exclude = [
         "target/**",
         "README.md",
diff --git a/rust/annotation.rs b/rust/annotation.rs
new file mode 100644
index 000000000..428964c9f
--- /dev/null
+++ b/rust/annotation.rs
@@ -0,0 +1,257 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{identifier::Identifier, token, Span},
+    util::write_joined,
+    value::Literal,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Annotation {
+    Abstract(Abstract),
+    Cardinality(Cardinality),
+    Cascade(Cascade),
+    Distinct(Distinct),
+    Independent(Independent),
+    Key(Key),
+    Range(Range),
+    Regex(Regex),
+    Subkey(Subkey),
+    Unique(Unique),
+    Values(Values),
+}
+
+impl fmt::Display for Annotation {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Abstract(inner) => fmt::Display::fmt(inner, f),
+            Self::Cardinality(inner) => fmt::Display::fmt(inner, f),
+            Self::Cascade(inner) => fmt::Display::fmt(inner, f),
+            Self::Distinct(inner) => fmt::Display::fmt(inner, f),
+            Self::Independent(inner) => fmt::Display::fmt(inner, f),
+            Self::Key(inner) => fmt::Display::fmt(inner, f),
+            Self::Range(inner) => fmt::Display::fmt(inner, f),
+            Self::Regex(inner) => fmt::Display::fmt(inner, f),
+            Self::Subkey(inner) => fmt::Display::fmt(inner, f),
+            Self::Unique(inner) => fmt::Display::fmt(inner, f),
+            Self::Values(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Abstract {
+    span: Option<Span>,
+}
+
+impl Abstract {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Abstract {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Abstract)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Cardinality {
+    span: Option<Span>,
+    min: Literal,
+    max: Option<Literal>,
+}
+
+impl Cardinality {
+    pub fn new(span: Option<Span>, min: Literal, max: Option<Literal>) -> Self {
+        Self { span, min, max }
+    }
+}
+
+impl fmt::Display for Cardinality {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}({}..", token::Annotation::Cardinality, self.min)?;
+        if let Some(max) = &self.max {
+            write!(f, "{}", max)?;
+        }
+        f.write_char(')')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Cascade {
+    span: Option<Span>,
+}
+
+impl Cascade {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Cascade {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Cascade)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Distinct {
+    span: Option<Span>,
+}
+
+impl Distinct {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Distinct {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Distinct)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Independent {
+    span: Option<Span>,
+}
+
+impl Independent {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Independent {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Independent)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Key {
+    span: Option<Span>,
+}
+
+impl Key {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Key {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Key)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Range {
+    span: Option<Span>,
+    min: Option<Literal>,
+    max: Option<Literal>,
+}
+
+impl Range {
+    pub fn new(span: Option<Span>, min: Option<Literal>, max: Option<Literal>) -> Self {
+        Self { span, min, max }
+    }
+}
+
+impl fmt::Display for Range {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}(", token::Annotation::Range)?;
+        if let Some(min) = &self.min {
+            write!(f, "{}", min)?;
+        }
+        f.write_str("..")?;
+        if let Some(max) = &self.max {
+            write!(f, "{}", max)?;
+        }
+        f.write_char(')')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Regex {
+    span: Option<Span>,
+    regex: Literal,
+}
+
+impl Regex {
+    pub fn new(span: Option<Span>, regex: Literal) -> Self {
+        Self { span, regex }
+    }
+}
+
+impl fmt::Display for Regex {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}({})", token::Annotation::Regex, self.regex)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Subkey {
+    span: Option<Span>,
+    ident: Identifier,
+}
+
+impl Subkey {
+    pub fn new(span: Option<Span>, ident: Identifier) -> Self {
+        Self { span, ident }
+    }
+}
+
+impl fmt::Display for Subkey {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}({})", token::Annotation::Subkey, self.ident)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Unique {
+    span: Option<Span>,
+}
+
+impl Unique {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl fmt::Display for Unique {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}", token::Annotation::Unique)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Values {
+    span: Option<Span>,
+    values: Vec<Literal>,
+}
+
+impl Values {
+    pub fn new(span: Option<Span>, values: Vec<Literal>) -> Self {
+        Self { span, values }
+    }
+}
+
+impl fmt::Display for Values {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{}(", token::Annotation::Values)?;
+        write_joined!(f, ", ", &self.values)?;
+        f.write_char(')')?;
+        Ok(())
+    }
+}
diff --git a/rust/builder/mod.rs b/rust/builder/mod.rs
index 9ae556c08..8c18d4f6a 100644
--- a/rust/builder/mod.rs
+++ b/rust/builder/mod.rs
@@ -4,12 +4,33 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-use crate::pattern::Label;
+use crate::{common::identifier::Identifier, type_::Label, variable::Variable};
 
 #[macro_export]
-macro_rules! typeql_define {
+macro_rules! define {
     ($($def:expr),* $(,)?) => {
-        $crate::query::TypeQLDefine::build(vec![$($def.into()),*])
+        $crate::query::schema::Define::build(vec![$($def.into()),*])
     }
 }
 
+#[macro_export]
+macro_rules! undefine {
+    ($($def:expr),* $(,)?) => {
+        $crate::query::schema::Undefine::build(vec![$($def.into()),*])
+    }
+}
+
+#[macro_export]
+macro_rules! match_ {
+    ($($pattern:expr),* $(,)?) => {
+        $crate::query::pipeline::stage::Match::build(vec![$($pattern.into()),*])
+    }
+}
+
+pub fn var(name: impl Into<Identifier>) -> Variable {
+    Variable::Named(None, name.into())
+}
+
+pub fn type_(name: impl Into<Identifier>) -> Label {
+    Label::Identifier(name.into())
+}
diff --git a/rust/common/error/mod.rs b/rust/common/error/mod.rs
index 2c07d1003..397dcfc61 100644
--- a/rust/common/error/mod.rs
+++ b/rust/common/error/mod.rs
@@ -9,7 +9,7 @@ use std::{error::Error as StdError, fmt};
 use itertools::Itertools;
 use pest::error::{Error as PestError, LineColLocation};
 
-use crate::{error_messages, write_joined};
+use crate::{error_messages, util::write_joined};
 
 #[macro_use]
 mod macros;
@@ -38,7 +38,7 @@ impl From<Vec<TypeQLError>> for Error {
 }
 
 pub(crate) fn syntax_error<T: pest::RuleType>(query: &str, error: PestError<T>) -> TypeQLError {
-    let (error_line_nr, _) = match error.line_col {
+    let (error_line_nr, error_col) = match error.line_col {
         LineColLocation::Pos((line, col)) => (line, col),
         LineColLocation::Span((line, col), _) => (line, col),
     };
@@ -55,7 +55,7 @@ pub(crate) fn syntax_error<T: pest::RuleType>(query: &str, error: PestError<T>)
             }
         })
         .join("\n");
-    TypeQLError::SyntaxErrorDetailed { error_line_nr, formatted_error }
+    TypeQLError::SyntaxErrorDetailed { error_line_nr, error_col, formatted_error }
 }
 
 impl fmt::Display for Error {
@@ -75,10 +75,14 @@ pub fn collect_err(i: impl IntoIterator<Item = Result<(), Error>>) -> Result<(),
 
 error_messages! { TypeQLError
     code: "TQL", type: "TypeQL Error",
-    SyntaxErrorDetailed { error_line_nr: usize, formatted_error: String } =
-        3: "There is a syntax error near line {error_line_nr}:\n{formatted_error}",
+    SyntaxErrorDetailed { error_line_nr: usize, error_col: usize, formatted_error: String } =
+        3: "There is a syntax error at {error_line_nr}:{error_col}:\n{formatted_error}",
     InvalidCasting { enum_name: &'static str, variant: &'static str, expected_variant: &'static str, typename: &'static str } =
         4: "Enum '{enum_name}::{variant}' does not match '{expected_variant}', and cannot be unwrapped into '{typename}'.",
+    InvalidLiteral { variant: &'static str, expected_variant: &'static str } =
+        5: "Attempting to parse a {variant} literal as {expected_variant}",
+    InvalidStringEscape { escape: String, full_string: String } =
+        6: "Encountered invalid escape sequence {escape:?} while parsing {full_string:?}.",
 /*
     MissingPatterns =
         5: "The query has not been provided with any patterns.",
diff --git a/rust/common/identifier.rs b/rust/common/identifier.rs
index 108f6ddc2..a9d46e593 100644
--- a/rust/common/identifier.rs
+++ b/rust/common/identifier.rs
@@ -4,10 +4,57 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-use std::sync::OnceLock;
+use std::{fmt, sync::OnceLock};
 
 use regex::{Regex, RegexBuilder};
 
+use crate::{
+    common::{Span, Spanned},
+    pretty::Pretty,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct Identifier {
+    span: Option<Span>,
+    ident: String,
+}
+
+impl Identifier {
+    pub fn new(span: Option<Span>, ident: String) -> Self {
+        Self { span, ident }
+    }
+
+    pub fn as_str(&self) -> &str {
+        &self.ident
+    }
+}
+
+impl Pretty for Identifier {}
+
+impl fmt::Display for Identifier {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(&self.ident, f)
+    }
+}
+
+impl Spanned for Identifier {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl From<&str> for Identifier {
+    fn from(value: &str) -> Self {
+        Self::new(None, value.to_owned())
+    }
+}
+
+impl From<String> for Identifier {
+    fn from(value: String) -> Self {
+        Self::new(None, value)
+    }
+}
+
 const IDENTIFIER_CHAR: &str = "A-Za-z\
             \\u00C0-\\u00D6\
             \\u00D8-\\u00F6\
@@ -27,7 +74,7 @@ const IDENTIFIER_CONNECTOR: &str = "_\
             \\u203F-\\u2040";
 const IDENTIFIER_DIGIT: &str = "0-9";
 
-pub fn is_valid_label_identifier(identifier: &str) -> bool {
+pub fn is_valid_identifier(identifier: &str) -> bool {
     static REGEX: OnceLock<Regex> = OnceLock::new();
     let regex = REGEX.get_or_init(|| {
         let identifier_tail = format!("{}{}{}", IDENTIFIER_CHAR, IDENTIFIER_CONNECTOR, IDENTIFIER_DIGIT);
diff --git a/rust/common/mod.rs b/rust/common/mod.rs
index 9be1d004a..8e1d5a31f 100644
--- a/rust/common/mod.rs
+++ b/rust/common/mod.rs
@@ -14,13 +14,13 @@ pub use error::Error;
 
 pub type Result<T = ()> = std::result::Result<T, Error>;
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 pub struct LineColumn {
-    pub line: usize,
-    pub column: usize,
+    pub line: u32,
+    pub column: u32,
 }
 
-#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
 pub struct Span {
     pub begin: LineColumn,
     pub end: LineColumn,
diff --git a/rust/common/string.rs b/rust/common/string.rs
index 19abc842d..18abebc3b 100644
--- a/rust/common/string.rs
+++ b/rust/common/string.rs
@@ -4,18 +4,12 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#![allow(dead_code)] // REASON: these will be used for builders
+
 pub(crate) fn quote(string: &str) -> String {
     format!("\"{string}\"")
 }
 
-pub(crate) fn unquote(quoted_string: &str) -> String {
-    String::from(&quoted_string[1..quoted_string.len() - 1])
-}
-
-pub(crate) fn indent(multiline_string: &str) -> String {
-    format!("    {}", multiline_string.replace('\n', "\n    "))
-}
-
 pub(crate) fn escape_regex(regex: &str) -> String {
     regex.replace('/', r"\/")
 }
diff --git a/rust/common/token.rs b/rust/common/token.rs
index cd56c89ed..39ea7e547 100644
--- a/rust/common/token.rs
+++ b/rust/common/token.rs
@@ -8,7 +8,7 @@ use std::fmt;
 
 macro_rules! string_enum {
     {$name:ident $($item:ident = $value:tt),* $(,)?} => {
-        #[derive(Debug, Clone, Copy, Eq, PartialEq)]
+        #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
         pub enum $name {
             $($item),*
         }
@@ -45,7 +45,6 @@ macro_rules! string_enum {
 }
 
 string_enum! { Type
-    Thing = "thing",
     Entity = "entity",
     Relation = "relation",
     Attribute = "attribute",
@@ -54,8 +53,11 @@ string_enum! { Type
 
 string_enum! { Clause
     Define = "define",
+    Redefine = "redefine",
     Undefine = "undefine",
     Insert = "insert",
+    Put = "put",
+    Update = "update",
     Delete = "delete",
     Match = "match",
     Group = "group",
@@ -64,6 +66,7 @@ string_enum! { Clause
 }
 
 string_enum! { Modifier
+    Filter = "filter",
     Sort = "sort",
     Offset = "offset",
     Limit = "limit",
@@ -107,31 +110,51 @@ string_enum! { Schema
     Then = "then",
 }
 
-string_enum! { Constraint
+string_enum! { Keyword
     Abstract = "abstract",
     As = "as",
+    Alias = "alias",
     Assign = "=",
+    From = "from",
+    Fun = "fun",
     Has = "has",
     IID = "iid",
+    In = "in",
     Is = "is",
     Isa = "isa",
     IsaX = "isa!",
+    Label = "label",
+    Links = "links",
+    Not = "not",
+    Of = "of",
+    Or = "or",
     Owns = "owns",
     Plays = "plays",
-    Regex = "regex",
     Relates = "relates",
+    Struct = "struct",
     Sub = "sub",
     SubX = "sub!",
-    Type = "type",
+    Try = "try",
     ValueType = "value",
 }
 
 string_enum! { Annotation
+    Abstract = "abstract",
+    Cardinality = "card",
+    Cascade = "cascade",
+    Distinct = "distinct",
+    Independent = "independent",
     Key = "key",
+    Range = "range",
+    Regex = "regex",
+    Subkey = "subkey",
     Unique = "unique",
+    Values = "values",
 }
 
 string_enum! { Aggregate
+    Check = "check",
+    First = "first",
     Count = "count",
     Max = "max",
     Mean = "mean",
@@ -143,8 +166,12 @@ string_enum! { Aggregate
 
 string_enum! { ValueType
     Boolean = "boolean",
+    Date = "date",
     DateTime = "datetime",
+    DateTimeTZ = "datetime-tz",
+    Decimal = "decimal",
     Double = "double",
+    Duration = "duration",
     Long = "long",
     String = "string",
 }
@@ -170,14 +197,11 @@ string_enum! { Function
     Max = "max",
     Min = "min",
     Round = "round",
-}
-
-string_enum! { Projection
-    As = "as",
+    Length = "length",
 }
 
 string_enum! { Char
-    // Question = "?",
+    Question = "?",
     Dollar = "$",
     Underscore = "_",
     CurlyLeft = "{",
diff --git a/rust/common/variables_retrieved.rs b/rust/common/variables_retrieved.rs
index a88e88154..cbc000d27 100644
--- a/rust/common/variables_retrieved.rs
+++ b/rust/common/variables_retrieved.rs
@@ -4,7 +4,6 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-
 use crate::variable::variable::VariableRef;
 
 pub trait VariablesRetrieved {
diff --git a/rust/expression/mod.rs b/rust/expression/mod.rs
new file mode 100644
index 000000000..492401465
--- /dev/null
+++ b/rust/expression/mod.rs
@@ -0,0 +1,284 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{
+        identifier::Identifier,
+        token::{self, ArithmeticOperator},
+        Span, Spanned,
+    },
+    pretty::Pretty,
+    util::write_joined,
+    value::Literal,
+    variable::Variable,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct BuiltinFunctionName {
+    span: Option<Span>,
+    token: token::Function,
+}
+
+impl BuiltinFunctionName {
+    pub fn new(span: Option<Span>, token: token::Function) -> Self {
+        Self { span, token }
+    }
+}
+
+impl Pretty for BuiltinFunctionName {}
+
+impl fmt::Display for BuiltinFunctionName {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.token)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum FunctionName {
+    Builtin(BuiltinFunctionName),
+    Identifier(Identifier),
+}
+
+impl Pretty for FunctionName {}
+
+impl fmt::Display for FunctionName {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Builtin(inner) => fmt::Display::fmt(inner, f),
+            Self::Identifier(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct FunctionCall {
+    span: Option<Span>,
+    name: FunctionName,
+    args: Vec<Expression>,
+}
+
+impl FunctionCall {
+    pub fn new(span: Option<Span>, name: FunctionName, args: Vec<Expression>) -> Self {
+        Self { span, name, args }
+    }
+}
+
+impl Spanned for FunctionCall {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for FunctionCall {}
+
+impl fmt::Display for FunctionCall {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}(", self.name)?;
+        write_joined!(f, ", ", self.args)?;
+        f.write_char(')')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Operation {
+    op: ArithmeticOperator,
+    left: Expression,
+    right: Expression,
+}
+
+impl Operation {
+    pub fn new(op: ArithmeticOperator, left: Expression, right: Expression) -> Self {
+        Self { op, left, right }
+    }
+}
+
+impl Spanned for Operation {
+    fn span(&self) -> Option<Span> {
+        if let (Some(left), Some(right)) = (self.left.span(), self.right.span()) {
+            Some(Span { begin: left.begin, end: right.end })
+        } else {
+            None
+        }
+    }
+}
+
+impl Pretty for Operation {}
+
+impl fmt::Display for Operation {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {} {}", self.left, self.op, self.right)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Paren {
+    span: Option<Span>,
+    inner: Expression,
+}
+
+impl Paren {
+    pub(crate) fn new(span: Option<Span>, inner: Expression) -> Self {
+        Self { span, inner }
+    }
+}
+
+impl Spanned for Paren {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Paren {}
+
+impl fmt::Display for Paren {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "({})", self.inner)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ListIndex {
+    span: Option<Span>,
+    variable: Variable,
+    index: Expression,
+}
+
+impl ListIndex {
+    pub fn new(span: Option<Span>, variable: Variable, index: Expression) -> Self {
+        Self { span, variable, index }
+    }
+}
+
+impl Spanned for ListIndex {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for ListIndex {}
+
+impl fmt::Display for ListIndex {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}[{}]", self.variable, self.index)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct List {
+    span: Option<Span>,
+    items: Vec<Expression>,
+}
+
+impl List {
+    pub fn new(span: Option<Span>, items: Vec<Expression>) -> Self {
+        Self { span, items }
+    }
+}
+
+impl Spanned for List {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for List {}
+
+impl fmt::Display for List {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_char('[')?;
+        write_joined!(f, ", ", self.items)?;
+        f.write_char(']')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ListIndexRange {
+    span: Option<Span>,
+    var: Variable,
+    from: Expression,
+    to: Expression,
+}
+
+impl ListIndexRange {
+    pub fn new(span: Option<Span>, var: Variable, from: Expression, to: Expression) -> Self {
+        Self { span, var, from, to }
+    }
+}
+
+impl Spanned for ListIndexRange {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for ListIndexRange {}
+
+impl fmt::Display for ListIndexRange {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}[{}..{}]", self.var, self.from, self.to)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Expression {
+    Variable(Variable),
+    ListIndex(Box<ListIndex>),
+    Value(Literal),
+    Function(FunctionCall),
+    Operation(Box<Operation>),
+    Paren(Box<Paren>),
+    List(List),
+    ListIndexRange(Box<ListIndexRange>),
+}
+
+impl Spanned for Expression {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Self::Variable(inner) => inner.span(),
+            Self::ListIndex(inner) => inner.span(),
+            Self::Value(inner) => inner.span(),
+            Self::Function(inner) => inner.span(),
+            Self::Operation(inner) => inner.span(),
+            Self::Paren(inner) => inner.span(),
+            Self::List(inner) => inner.span(),
+            Self::ListIndexRange(inner) => inner.span(),
+        }
+    }
+}
+
+impl Pretty for Expression {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::ListIndex(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Value(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Function(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Operation(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Paren(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::List(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::ListIndexRange(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Expression {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => fmt::Display::fmt(inner, f),
+            Self::ListIndex(inner) => fmt::Display::fmt(inner, f),
+            Self::Value(inner) => fmt::Display::fmt(inner, f),
+            Self::Function(inner) => fmt::Display::fmt(inner, f),
+            Self::Operation(inner) => fmt::Display::fmt(inner, f),
+            Self::Paren(inner) => fmt::Display::fmt(inner, f),
+            Self::List(inner) => fmt::Display::fmt(inner, f),
+            Self::ListIndexRange(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/parser/annotation.rs b/rust/parser/annotation.rs
new file mode 100644
index 000000000..f2d6837ff
--- /dev/null
+++ b/rust/parser/annotation.rs
@@ -0,0 +1,125 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::{
+    visit_identifier, visit_integer_literal, visit_quoted_string_literal, visit_value_literal, IntoChildNodes, Node,
+    Rule, RuleMatcher,
+};
+use crate::{
+    annotation::{
+        Abstract, Annotation, Cardinality, Cascade, Distinct, Independent, Key, Range, Regex, Subkey, Unique, Values,
+    },
+    common::{error::TypeQLError, Spanned},
+    value::Literal,
+};
+
+pub(super) fn visit_annotations(node: Node<'_>) -> Vec<Annotation> {
+    debug_assert_eq!(node.as_rule(), Rule::annotations);
+    node.into_children().map(visit_annotation).collect()
+}
+
+fn visit_annotation(node: Node<'_>) -> Annotation {
+    debug_assert_eq!(node.as_rule(), Rule::annotation);
+    let span = node.span();
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::ANNOTATION_ABSTRACT => Annotation::Abstract(Abstract::new(span)),
+        Rule::ANNOTATION_CASCADE => Annotation::Cascade(Cascade::new(span)),
+        Rule::ANNOTATION_DISTINCT => Annotation::Distinct(Distinct::new(span)),
+        Rule::ANNOTATION_INDEPENDENT => Annotation::Independent(Independent::new(span)),
+        Rule::ANNOTATION_KEY => Annotation::Key(Key::new(span)),
+        Rule::ANNOTATION_UNIQUE => Annotation::Unique(Unique::new(span)),
+        Rule::annotation_card => Annotation::Cardinality(visit_annotation_card(child)),
+        Rule::annotation_range => Annotation::Range(visit_annotation_range(child)),
+        Rule::annotation_regex => Annotation::Regex(visit_annotation_regex(child)),
+        Rule::annotation_subkey => Annotation::Subkey(visit_annotation_subkey(child)),
+        Rule::annotation_values => Annotation::Values(visit_annotation_values(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_annotation_card(node: Node<'_>) -> Cardinality {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_card);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::ANNOTATION_CARD);
+    let lower = visit_integer_literal(children.consume_expected(Rule::integer_literal));
+    let upper = children.try_consume_expected(Rule::integer_literal).map(visit_integer_literal);
+    debug_assert_eq!(children.try_consume_any(), None);
+    Cardinality::new(span, lower, upper)
+}
+
+fn visit_annotation_range(node: Node<'_>) -> Range {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_range);
+    let span = node.span();
+    let mut children = node.into_children();
+    let (lower, upper) = visit_range(children.skip_expected(Rule::ANNOTATION_RANGE).consume_expected(Rule::range));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Range::new(span, lower, upper)
+}
+
+fn visit_range(node: Node<'_>) -> (Option<Literal>, Option<Literal>) {
+    debug_assert_eq!(node.as_rule(), Rule::range);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::range_full => visit_range_full(child),
+        Rule::range_from => visit_range_from(child),
+        Rule::range_to => visit_range_to(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_range_full(node: Node<'_>) -> (Option<Literal>, Option<Literal>) {
+    debug_assert_eq!(node.as_rule(), Rule::range_full);
+    let mut children = node.into_children();
+    let lower = visit_value_literal(children.consume_expected(Rule::value_literal));
+    let upper = visit_value_literal(children.consume_expected(Rule::value_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    (Some(lower), Some(upper))
+}
+
+fn visit_range_from(node: Node<'_>) -> (Option<Literal>, Option<Literal>) {
+    debug_assert_eq!(node.as_rule(), Rule::range_from);
+    let mut children = node.into_children();
+    let lower = visit_value_literal(children.consume_expected(Rule::value_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    (Some(lower), None)
+}
+
+fn visit_range_to(node: Node<'_>) -> (Option<Literal>, Option<Literal>) {
+    debug_assert_eq!(node.as_rule(), Rule::range_to);
+    let mut children = node.into_children();
+    let upper = visit_value_literal(children.consume_expected(Rule::value_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    (None, Some(upper))
+}
+
+fn visit_annotation_subkey(node: Node<'_>) -> Subkey {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_subkey);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.consume_expected(Rule::ANNOTATION_SUBKEY);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Subkey::new(span, ident)
+}
+
+fn visit_annotation_regex(node: Node<'_>) -> Regex {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_regex);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.consume_expected(Rule::ANNOTATION_REGEX);
+    let regex = visit_quoted_string_literal(children.consume_expected(Rule::quoted_string_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Regex::new(span, regex)
+}
+
+fn visit_annotation_values(node: Node<'_>) -> Values {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_values);
+    let span = node.span();
+    let values = node.into_children().skip_expected(Rule::ANNOTATION_VALUES).map(visit_value_literal).collect();
+    Values::new(span, values)
+}
diff --git a/rust/parser/define/function.rs b/rust/parser/define/function.rs
new file mode 100644
index 000000000..bfefd8611
--- /dev/null
+++ b/rust/parser/define/function.rs
@@ -0,0 +1,146 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use itertools::Itertools;
+
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    parser::{
+        pipeline::{visit_reduce, visit_stage_match, visit_stage_modifier},
+        visit_identifier, visit_label, visit_label_list, visit_value_type, visit_value_type_list,
+        visit_value_type_optional, visit_var, visit_vars, IntoChildNodes, Node, Rule, RuleMatcher,
+    },
+    schema::definable::{
+        function::{
+            Argument, Output, ReturnSingle, ReturnStatement, ReturnStream, Signature, Single, SingleOutput, Stream,
+        },
+        Function,
+    },
+    type_::{Type, TypeAny},
+};
+
+pub(in crate::parser) fn visit_definition_function(node: Node<'_>) -> Function {
+    debug_assert_eq!(node.as_rule(), Rule::definition_function);
+    let span = node.span();
+    let mut children = node.into_children();
+
+    children.skip_expected(Rule::FUN);
+    let signature = visit_function_signature(children.consume_expected(Rule::function_signature));
+    let body = visit_stage_match(children.consume_expected(Rule::stage_match));
+    let modifiers =
+        children.take_while_ref(|node| node.as_rule() == Rule::stage_modifier).map(visit_stage_modifier).collect();
+    let return_stmt = visit_return_statement(children.consume_expected(Rule::return_statement));
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Function::new(span, signature, body, modifiers, return_stmt)
+}
+
+fn visit_return_statement(node: Node<'_>) -> ReturnStatement {
+    debug_assert_eq!(node.as_rule(), Rule::return_statement);
+    let mut children = node.into_children();
+
+    children.skip_expected(Rule::RETURN);
+    let child = children.consume_any();
+    let return_stmt = match child.as_rule() {
+        Rule::return_statement_stream => ReturnStatement::Stream(visit_return_statement_stream(child)),
+        Rule::return_statement_single => ReturnStatement::Single(visit_return_statement_single(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    return_stmt
+}
+
+fn visit_return_statement_single(node: Node<'_>) -> ReturnSingle {
+    debug_assert_eq!(node.as_rule(), Rule::return_statement_single);
+    let span = node.span();
+    let outputs = node
+        .into_children()
+        .map(|child| match child.as_rule() {
+            Rule::var => SingleOutput::Variable(visit_var(child)),
+            Rule::reduce => SingleOutput::Reduce(visit_reduce(child)),
+            _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+        })
+        .collect();
+    ReturnSingle::new(span, outputs)
+}
+
+fn visit_return_statement_stream(node: Node<'_>) -> ReturnStream {
+    debug_assert_eq!(node.as_rule(), Rule::return_statement_stream);
+    ReturnStream::new(node.span(), visit_vars(node.into_child()))
+}
+
+fn visit_function_signature(node: Node<'_>) -> Signature {
+    debug_assert_eq!(node.as_rule(), Rule::function_signature);
+    let span = node.span();
+    let mut children = node.into_children();
+    let sigil = visit_identifier(children.consume_expected(Rule::identifier));
+    let args = visit_function_arguments(children.consume_expected(Rule::function_arguments));
+    let return_types = visit_function_output(children.consume_expected(Rule::function_output));
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Signature::new(span, sigil, args, return_types)
+}
+
+fn visit_function_output(node: Node<'_>) -> Output {
+    debug_assert_eq!(node.as_rule(), Rule::function_output);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::function_output_stream => Output::Stream(visit_function_output_stream(child)),
+        Rule::function_output_single => Output::Single(visit_function_output_single(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_function_output_stream(node: Node<'_>) -> Stream {
+    debug_assert_eq!(node.as_rule(), Rule::function_output_stream);
+    let span = node.span();
+    Stream::new(span, node.into_children().map(visit_function_output_type).collect())
+}
+
+fn visit_function_output_single(node: Node<'_>) -> Single {
+    debug_assert_eq!(node.as_rule(), Rule::function_output_single);
+    let span = node.span();
+    Single::new(span, node.into_children().map(visit_function_output_type).collect())
+}
+
+fn visit_function_output_type(node: Node<'_>) -> TypeAny {
+    debug_assert_eq!(node.as_rule(), Rule::function_output_type);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::value_type => TypeAny::Type(visit_value_type(child)),
+        Rule::value_type_optional => TypeAny::Optional(visit_value_type_optional(child)),
+        Rule::value_type_list => TypeAny::List(visit_value_type_list(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_function_arguments(node: Node<'_>) -> Vec<Argument> {
+    debug_assert_eq!(node.as_rule(), Rule::function_arguments);
+    node.into_children().map(visit_function_argument).collect()
+}
+
+fn visit_function_argument(node: Node<'_>) -> Argument {
+    debug_assert_eq!(node.as_rule(), Rule::function_argument);
+    let span = node.span();
+    let mut children = node.into_children();
+
+    let var = visit_var(children.consume_expected(Rule::var));
+    let type_ = visit_label_arg(children.consume_expected(Rule::label_arg));
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Argument::new(span, var, type_)
+}
+
+fn visit_label_arg(node: Node<'_>) -> TypeAny {
+    debug_assert_eq!(node.as_rule(), Rule::label_arg);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::label => TypeAny::Type(Type::Label(visit_label(child))),
+        Rule::label_list => TypeAny::List(visit_label_list(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
diff --git a/rust/parser/define/mod.rs b/rust/parser/define/mod.rs
new file mode 100644
index 000000000..ce2a88655
--- /dev/null
+++ b/rust/parser/define/mod.rs
@@ -0,0 +1,43 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+pub(super) mod function;
+pub(super) mod struct_;
+pub(super) mod type_;
+
+use self::{function::visit_definition_function, struct_::visit_definition_struct, type_::visit_definition_type};
+use super::{IntoChildNodes, Node, Rule, RuleMatcher};
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    query::schema::Define,
+    schema::definable::Definable,
+};
+
+pub(super) fn visit_query_define(node: Node<'_>) -> Define {
+    debug_assert_eq!(node.as_rule(), Rule::query_define);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::DEFINE);
+    let definables = visit_definables(children.consume_expected(Rule::definables));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Define::new(span, definables)
+}
+
+pub(super) fn visit_definables(node: Node<'_>) -> Vec<Definable> {
+    debug_assert_eq!(node.as_rule(), Rule::definables);
+    node.into_children().map(visit_definable).collect()
+}
+
+pub(super) fn visit_definable(node: Node<'_>) -> Definable {
+    debug_assert_eq!(node.as_rule(), Rule::definable);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::definition_type => Definable::TypeDeclaration(visit_definition_type(child)),
+        Rule::definition_function => Definable::Function(visit_definition_function(child)),
+        Rule::definition_struct => Definable::Struct(visit_definition_struct(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
diff --git a/rust/parser/define/struct_.rs b/rust/parser/define/struct_.rs
new file mode 100644
index 000000000..ab5233003
--- /dev/null
+++ b/rust/parser/define/struct_.rs
@@ -0,0 +1,49 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    parser::{visit_identifier, visit_value_type, visit_value_type_optional, IntoChildNodes, Node, Rule, RuleMatcher},
+    schema::definable::{struct_::Field, Struct},
+    type_::TypeAny,
+};
+
+pub(in crate::parser) fn visit_definition_struct(node: Node<'_>) -> Struct {
+    debug_assert_eq!(node.as_rule(), Rule::definition_struct);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::STRUCT);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    let fields = visit_definition_struct_fields(children.consume_expected(Rule::definition_struct_fields));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Struct::new(span, ident, fields)
+}
+
+fn visit_definition_struct_fields(node: Node<'_>) -> Vec<Field> {
+    debug_assert_eq!(node.as_rule(), Rule::definition_struct_fields);
+    node.into_children().map(visit_definition_struct_field).collect()
+}
+
+fn visit_definition_struct_field(node: Node<'_>) -> Field {
+    debug_assert_eq!(node.as_rule(), Rule::definition_struct_field);
+    let span = node.span();
+    let mut children = node.into_children();
+    let key = visit_identifier(children.consume_expected(Rule::identifier));
+    children.skip_expected(Rule::VALUE);
+    let type_ = visit_struct_field_value_type(children.consume_expected(Rule::struct_field_value_type));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Field::new(span, key, type_)
+}
+
+fn visit_struct_field_value_type(node: Node<'_>) -> TypeAny {
+    debug_assert_eq!(node.as_rule(), Rule::struct_field_value_type);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::value_type => TypeAny::Type(visit_value_type(child)),
+        Rule::value_type_optional => TypeAny::Optional(visit_value_type_optional(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
diff --git a/rust/parser/define/type_.rs b/rust/parser/define/type_.rs
new file mode 100644
index 000000000..9b7abf90d
--- /dev/null
+++ b/rust/parser/define/type_.rs
@@ -0,0 +1,144 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    parser::{
+        annotation::visit_annotations, visit_identifier, visit_kind, visit_label, visit_label_list, visit_label_scoped,
+        visit_value_type, IntoChildNodes, Node, Rule, RuleMatcher,
+    },
+    schema::definable::type_::{
+        capability::{Alias, Owns, Plays, Relates, Sub, ValueType},
+        Capability, CapabilityBase, Type,
+    },
+    type_::{Label, Type as TypeRef, TypeAny},
+};
+
+pub(super) fn visit_definition_type(node: Node<'_>) -> Type {
+    debug_assert_eq!(node.as_rule(), Rule::definition_type);
+    let span = node.span();
+    let mut children = node.into_children();
+    let kind = children.try_consume_expected(Rule::kind).map(visit_kind);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    let traits = children.map(visit_type_capability).collect();
+    Type::new(span, kind, Label::Identifier(ident), traits)
+}
+
+pub(in crate::parser) fn visit_type_capability(node: Node<'_>) -> Capability {
+    debug_assert_eq!(node.as_rule(), Rule::type_capability);
+    let span = node.span();
+    let mut children = node.into_children();
+    let base = visit_type_capability_base(children.consume_expected(Rule::type_capability_base));
+    let annotations = children.try_consume_expected(Rule::annotations).map(visit_annotations).unwrap_or_default();
+    debug_assert_eq!(children.try_consume_any(), None);
+    Capability::new(span, base, annotations)
+}
+
+pub(in crate::parser) fn visit_type_capability_base(node: Node<'_>) -> CapabilityBase {
+    debug_assert_eq!(node.as_rule(), Rule::type_capability_base);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::sub_declaration => CapabilityBase::Sub(visit_sub_declaration(child)),
+        Rule::alias_declaration => CapabilityBase::Alias(visit_alias_declaration(child)),
+        Rule::value_type_declaration => CapabilityBase::ValueType(visit_value_type_declaration(child)),
+        Rule::owns_declaration => CapabilityBase::Owns(visit_owns_declaration(child)),
+        Rule::relates_declaration => CapabilityBase::Relates(visit_relates_declaration(child)),
+        Rule::plays_declaration => CapabilityBase::Plays(visit_plays_declaration(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_sub_declaration(node: Node<'_>) -> Sub {
+    debug_assert_eq!(node.as_rule(), Rule::sub_declaration);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::SUB);
+    let supertype_label = visit_label(children.consume_expected(Rule::label));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Sub::new(span, supertype_label)
+}
+
+fn visit_alias_declaration(node: Node<'_>) -> Alias {
+    debug_assert_eq!(node.as_rule(), Rule::alias_declaration);
+    let span = node.span();
+    let aliases = node.into_children().skip_expected(Rule::ALIAS).map(visit_label).collect();
+    Alias::new(span, aliases)
+}
+
+fn visit_value_type_declaration(node: Node<'_>) -> ValueType {
+    debug_assert_eq!(node.as_rule(), Rule::value_type_declaration);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::VALUE);
+    let value_type_node = children.consume_any();
+    let value_type = match value_type_node.as_rule() {
+        Rule::label => TypeRef::Label(visit_label(value_type_node)),
+        Rule::value_type => visit_value_type(value_type_node),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: value_type_node.to_string() }),
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    ValueType::new(span, value_type)
+}
+
+fn visit_owns_declaration(node: Node<'_>) -> Owns {
+    debug_assert_eq!(node.as_rule(), Rule::owns_declaration);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::OWNS);
+
+    let owned_label = children.consume_any();
+    let owned = match owned_label.as_rule() {
+        Rule::label_list => TypeAny::List(visit_label_list(owned_label)),
+        Rule::label => TypeAny::Type(TypeRef::Label(visit_label(owned_label))),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: owned_label.to_string() }),
+    };
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_label(children.consume_expected(Rule::label)))
+    } else {
+        None
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Owns::new(span, owned, overridden)
+}
+
+fn visit_relates_declaration(node: Node<'_>) -> Relates {
+    debug_assert_eq!(node.as_rule(), Rule::relates_declaration);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::RELATES);
+
+    let related_label = children.consume_any();
+    let related = match related_label.as_rule() {
+        Rule::label_list => TypeAny::List(visit_label_list(related_label)),
+        Rule::label => TypeAny::Type(TypeRef::Label(visit_label(related_label))),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: related_label.to_string() }),
+    };
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_label(children.consume_expected(Rule::label)))
+    } else {
+        None
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    Relates::new(span, related, overridden)
+}
+
+fn visit_plays_declaration(node: Node<'_>) -> Plays {
+    debug_assert_eq!(node.as_rule(), Rule::plays_declaration);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::PLAYS);
+
+    let role = visit_label_scoped(children.consume_expected(Rule::label_scoped));
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_label(children.consume_expected(Rule::label)))
+    } else {
+        None
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Plays::new(span, role, overridden)
+}
diff --git a/rust/parser/expression.rs b/rust/parser/expression.rs
new file mode 100644
index 000000000..e41364e01
--- /dev/null
+++ b/rust/parser/expression.rs
@@ -0,0 +1,169 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use pest::pratt_parser::{Assoc, Op, PrattParser};
+
+use super::{visit_identifier, visit_value_literal, visit_var, IntoChildNodes, Node, Rule, RuleMatcher};
+use crate::{
+    common::{error::TypeQLError, token, Spanned},
+    expression::{
+        BuiltinFunctionName, Expression, FunctionCall, FunctionName, List, ListIndex, ListIndexRange, Operation, Paren,
+    },
+    value::Literal,
+};
+
+pub(super) fn visit_expression_function(node: Node<'_>) -> FunctionCall {
+    debug_assert_eq!(node.as_rule(), Rule::expression_function);
+    let span = node.span();
+    let mut children = node.into_children();
+    let name = visit_expression_function_name(children.consume_expected(Rule::expression_function_name));
+    let args =
+        children.try_consume_expected(Rule::expression_arguments).map(visit_expression_arguments).unwrap_or_default();
+    debug_assert_eq!(children.try_consume_any(), None);
+    FunctionCall::new(span, name, args)
+}
+
+fn visit_expression_arguments(node: Node<'_>) -> Vec<Expression> {
+    debug_assert_eq!(node.as_rule(), Rule::expression_arguments);
+    node.into_children().map(visit_expression).collect()
+}
+
+pub(super) fn visit_expression(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::expression_value => visit_expression_value(child),
+        Rule::expression_list => visit_expression_list(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+pub(super) fn visit_expression_value(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression_value);
+
+    let pratt_parser: PrattParser<Rule> = PrattParser::new()
+        .op(Op::infix(Rule::PLUS, Assoc::Left) | Op::infix(Rule::MINUS, Assoc::Left))
+        .op(Op::infix(Rule::TIMES, Assoc::Left)
+            | Op::infix(Rule::DIVIDE, Assoc::Left)
+            | Op::infix(Rule::MODULO, Assoc::Left))
+        .op(Op::infix(Rule::POWER, Assoc::Right));
+
+    pratt_parser
+        .map_primary(visit_expression_base)
+        .map_infix(|left, op, right| {
+            let op = match op.as_rule() {
+                Rule::PLUS => token::ArithmeticOperator::Add,
+                Rule::MINUS => token::ArithmeticOperator::Subtract,
+                Rule::TIMES => token::ArithmeticOperator::Multiply,
+                Rule::DIVIDE => token::ArithmeticOperator::Divide,
+                Rule::MODULO => token::ArithmeticOperator::Modulo,
+                Rule::POWER => token::ArithmeticOperator::Power,
+                _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: op.to_string() }),
+            };
+            Expression::Operation(Box::new(Operation::new(op, left, right)))
+        })
+        .parse(node.into_children())
+}
+
+fn visit_expression_base(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression_base);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var => Expression::Variable(visit_var(child)),
+        Rule::value_literal => Expression::Value(visit_value_literal(child)),
+        Rule::expression_function => Expression::Function(visit_expression_function(child)),
+        Rule::expression_parenthesis => Expression::Paren(Box::new(visit_expression_parenthesis(child))),
+        Rule::expression_list_index => Expression::ListIndex(Box::new(visit_expression_list_index(child))),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_expression_list_index(node: Node<'_>) -> ListIndex {
+    debug_assert_eq!(node.as_rule(), Rule::expression_list_index);
+    let span = node.span();
+    let mut children = node.into_children();
+    let variable = visit_var(children.consume_expected(Rule::var));
+    let index = visit_list_index(children.consume_expected(Rule::list_index));
+    ListIndex::new(span, variable, index)
+}
+
+fn visit_list_index(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::list_index);
+    visit_expression_value(node.into_child())
+}
+
+pub(super) fn visit_expression_struct(node: Node<'_>) -> Literal {
+    debug_assert_eq!(node.as_rule(), Rule::expression_struct);
+    Literal::new(node.span(), None, node.as_str().to_owned()) // TODO parse properly
+}
+
+fn visit_expression_parenthesis(node: Node<'_>) -> Paren {
+    debug_assert_eq!(node.as_rule(), Rule::expression_parenthesis);
+    Paren::new(node.span(), visit_expression_value(node.into_child()))
+}
+
+pub(super) fn visit_expression_list(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression_list);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::expression_list_new => visit_expression_list_new(child),
+        Rule::expression_list_subrange => visit_expression_list_subrange(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_expression_list_subrange(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression_list_subrange);
+    let span = node.span();
+    let mut children = node.into_children();
+    let var = visit_var(children.consume_expected(Rule::var));
+    let (from, to) = visit_list_range(children.consume_expected(Rule::list_range));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Expression::ListIndexRange(Box::new(ListIndexRange::new(span, var, from, to)))
+}
+
+fn visit_list_range(node: Node<'_>) -> (Expression, Expression) {
+    debug_assert_eq!(node.as_rule(), Rule::list_range);
+    let mut children = node.into_children();
+    let from = visit_expression_value(children.consume_expected(Rule::expression_value));
+    let to = visit_expression_value(children.consume_expected(Rule::expression_value));
+    debug_assert_eq!(children.try_consume_any(), None);
+    (from, to)
+}
+
+fn visit_expression_list_new(node: Node<'_>) -> Expression {
+    debug_assert_eq!(node.as_rule(), Rule::expression_list_new);
+    let span = node.span();
+    let items = node.into_children().map(visit_expression_value).collect();
+    Expression::List(List::new(span, items))
+}
+
+fn visit_expression_function_name(node: Node<'_>) -> FunctionName {
+    debug_assert_eq!(node.as_rule(), Rule::expression_function_name);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::identifier => FunctionName::Identifier(visit_identifier(child)),
+        Rule::builtin_func_name => FunctionName::Builtin(visit_builtin_func_name(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_builtin_func_name(node: Node<'_>) -> BuiltinFunctionName {
+    debug_assert_eq!(node.as_rule(), Rule::builtin_func_name);
+    let span = node.span();
+    let child = node.into_child();
+    let token = match child.as_rule() {
+        Rule::ABS => token::Function::Abs,
+        Rule::CEIL => token::Function::Ceil,
+        Rule::FLOOR => token::Function::Floor,
+        Rule::LENGTH => token::Function::Length,
+        Rule::MAX => token::Function::Max,
+        Rule::MIN => token::Function::Min,
+        Rule::ROUND => token::Function::Round,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+    BuiltinFunctionName::new(span, token)
+}
diff --git a/rust/parser/mod.rs b/rust/parser/mod.rs
index 7ec4fa273..48db8b433 100644
--- a/rust/parser/mod.rs
+++ b/rust/parser/mod.rs
@@ -7,21 +7,33 @@
 use pest::Parser;
 use pest_derive::Parser;
 
+use self::{
+    define::{function::visit_definition_function, struct_::visit_definition_struct, visit_query_define},
+    undefine::visit_query_undefine,
+};
 use crate::{
     common::{
         error::{syntax_error, TypeQLError},
-        LineColumn, Span, Spanned,
-    },
-    pattern::{
-        statement::type_::declaration::{
-            AnnotationOwns, AnnotationRelates, AnnotationSub, AnnotationValueType, Owned, OwnsDeclaration, Played, PlaysDeclaration, Related, RelatesDeclaration, SubDeclaration, ValueTypeDeclaration
-        },
-        Definable, Label, TypeDeclaration,
+        identifier::Identifier,
+        token, LineColumn, Span, Spanned,
     },
-    query::{Query, SchemaQuery, TypeQLDefine},
+    parser::{pipeline::visit_query_pipeline, redefine::visit_query_redefine},
+    query::{Query, SchemaQuery},
+    schema::definable,
+    type_::{BuiltinValueType, Label, List, Optional, ReservedLabel, ScopedLabel, Type},
+    value::{Literal, Tag},
+    variable::Variable,
     Result,
 };
 
+mod annotation;
+mod define;
+mod expression;
+mod pipeline;
+mod redefine;
+mod statement;
+mod undefine;
+
 #[cfg(test)]
 mod test;
 
@@ -37,25 +49,25 @@ impl Spanned for Node<'_> {
         let (begin_line, begin_col) = self.as_span().start_pos().line_col();
         let (end_line, end_col) = self.as_span().end_pos().line_col();
         Some(Span {
-            begin: LineColumn { line: begin_line, column: begin_col },
-            end: LineColumn { line: end_line, column: end_col },
+            begin: LineColumn { line: begin_line as u32, column: begin_col as u32 },
+            end: LineColumn { line: end_line as u32, column: end_col as u32 },
         })
     }
 }
 
 trait IntoChildNodes<'a> {
-    fn into_child(self) -> Result<Node<'a>>;
+    fn into_child(self) -> Node<'a>;
     fn into_children(self) -> ChildNodes<'a>;
 }
 
 impl<'a> IntoChildNodes<'a> for Node<'a> {
-    fn into_child(self) -> Result<Node<'a>> {
+    fn into_child(self) -> Node<'a> {
         let mut children = self.into_children();
         let child = children.consume_any();
         match children.try_consume_any() {
-            None => Ok(child),
+            None => child,
             Some(next) => {
-                Err(TypeQLError::IllegalGrammar { input: format!("{child} is followed by more tokens: {next}") }.into())
+                unreachable!("{child} is followed by more tokens: {next}")
             }
         }
     }
@@ -119,49 +131,26 @@ pub(crate) fn visit_eof_query(query: &str) -> Result<Query> {
     Ok(visit_query(parse_single(Rule::eof_query, query)?.into_children().consume_expected(Rule::query)))
 }
 
-// pub(crate) fn visit_eof_queries(queries: &str) -> Result<impl Iterator<Item = Result<Query>> + '_> {
-//     Ok(parse(Rule::eof_queries, queries)?
-//         .consume_expected(Rule::eof_queries)
-//         .into_children()
-//         .filter(|child| matches!(child.as_rule(), Rule::query))
-//         .map(|query| visit_query(query).validated()))
-// }
-
-// pub(crate) fn visit_eof_pattern(pattern: &str) -> Result<Pattern> {
-//     visit_pattern(parse_single(Rule::eof_pattern, pattern)?.into_children().consume_expected(Rule::pattern)).validated()
-// }
-
-// pub(crate) fn visit_eof_patterns(patterns: &str) -> Result<Vec<Pattern>> {
-//     visit_patterns(parse_single(Rule::eof_patterns, patterns)?.into_children().consume_expected(Rule::patterns))
-//         .into_iter()
-//         .map(Validatable::validated)
-//         .collect()
-// }
-
-// pub(crate) fn visit_eof_definables(definables: &str) -> Result<Vec<Definable>> {
-//     visit_definables(parse_single(Rule::eof_definables, definables)?.into_children().consume_expected(Rule::definables))
-//         .into_iter()
-//         .map(Validatable::validated)
-//         .collect()
-// }
-
-// pub(crate) fn visit_eof_statement(statement: &str) -> Result<Statement> {
-//     visit_statement(parse_single(Rule::eof_statement, statement)?.into_children().consume_expected(Rule::statement))
-//         .validated()
-// }
-
-// pub(crate) fn visit_eof_label(label: &str) -> Result<Label> {
-//     let parsed = parse_single(Rule::eof_label, label)?.into_children().consume_expected(Rule::label);
-//     let string = parsed.as_str();
-//     if string != label {
-//         Err(TypeQLError::InvalidTypeLabel { label: label.to_string() })?;
-//     }
-//     Ok(string.into())
-// }
-
-// pub(crate) fn visit_eof_schema_rule(rule: &str) -> Result<crate::pattern::Rule> {
-//     visit_schema_rule(parse_single(Rule::eof_schema_rule, rule)?).validated()
-// }
+pub(crate) fn visit_eof_definition_function(query: &str) -> Result<definable::Function> {
+    Ok(visit_definition_function(
+        parse_single(Rule::eof_definition_function, query)?.into_children().consume_expected(Rule::definition_function),
+    ))
+}
+
+pub(crate) fn visit_eof_definition_struct(query: &str) -> Result<definable::Struct> {
+    Ok(visit_definition_struct(
+        parse_single(Rule::eof_definition_struct, query)?.into_children().consume_expected(Rule::definition_struct),
+    ))
+}
+
+pub(crate) fn visit_eof_label(label: &str) -> Result<Label> {
+    let parsed = parse_single(Rule::eof_label, label)?.into_children().consume_expected(Rule::label);
+    let string = parsed.as_str();
+    if string != label {
+        Err(TypeQLError::InvalidTypeLabel { label: label.to_string() })?;
+    }
+    Ok(visit_label(parsed))
+}
 
 fn visit_query(node: Node<'_>) -> Query {
     debug_assert_eq!(node.as_rule(), Rule::query);
@@ -169,10 +158,10 @@ fn visit_query(node: Node<'_>) -> Query {
     let child = children.consume_any();
     let query = match child.as_rule() {
         Rule::query_schema => Query::Schema(visit_query_schema(child)),
-        Rule::query_data => todo!(),
+        Rule::query_pipeline => Query::Pipeline(visit_query_pipeline(child)),
         _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
     };
-    debug_assert!(children.try_consume_any().is_none());
+    debug_assert_eq!(children.try_consume_any(), None);
     query
 }
 
@@ -182,224 +171,149 @@ fn visit_query_schema(node: Node<'_>) -> SchemaQuery {
     let child = children.consume_any();
     let query = match child.as_rule() {
         Rule::query_define => SchemaQuery::Define(visit_query_define(child)),
-        Rule::query_undefine => todo!(),
+        Rule::query_redefine => SchemaQuery::Redefine(visit_query_redefine(child)),
+        Rule::query_undefine => SchemaQuery::Undefine(visit_query_undefine(child)),
         _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
     };
-    debug_assert!(children.try_consume_any().is_none());
-    query
-}
-
-fn visit_query_define(node: Node<'_>) -> TypeQLDefine {
-    debug_assert_eq!(node.as_rule(), Rule::query_define);
-    let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::DEFINE);
-    let query = TypeQLDefine::new(visit_definables(children.consume_expected(Rule::definables)), span);
-    debug_assert!(children.try_consume_any().is_none());
+    debug_assert_eq!(children.try_consume_any(), None);
     query
 }
 
-fn visit_definables(node: Node<'_>) -> Vec<Definable> {
-    debug_assert_eq!(node.as_rule(), Rule::definables);
-    node.into_children().map(visit_definable).collect()
+fn visit_label(node: Node<'_>) -> Label {
+    debug_assert_eq!(node.as_rule(), Rule::label);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::identifier => Label::Identifier(visit_identifier(child)),
+        Rule::kind => Label::Reserved(visit_kind(child)),
+        Rule::ROLE => Label::Reserved(ReservedLabel::new(child.span(), token::Type::Role)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
 }
 
-fn visit_definable(node: Node<'_>) -> Definable {
-    debug_assert_eq!(node.as_rule(), Rule::definable);
-    let mut children = node.into_children();
-    let child = children.consume_any();
-    let definable = match child.as_rule() {
-        Rule::definition_type => Definable::TypeDeclaration(visit_definition_type(child)),
-        Rule::definition_function => todo!(),
-        Rule::definition_struct => todo!(),
+fn visit_kind(node: Node<'_>) -> ReservedLabel {
+    debug_assert_eq!(node.as_rule(), Rule::kind);
+    let span = node.span();
+    let child = node.into_child();
+    let token = match child.as_rule() {
+        Rule::ENTITY => token::Type::Entity,
+        Rule::RELATION => token::Type::Relation,
+        Rule::ATTRIBUTE => token::Type::Attribute,
+        Rule::ROLE => token::Type::Role,
         _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
     };
-    debug_assert!(children.try_consume_any().is_none());
-    definable
+    ReservedLabel::new(span, token)
 }
 
-fn visit_definition_type(node: Node<'_>) -> TypeDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::definition_type);
+fn visit_label_scoped(node: Node<'_>) -> ScopedLabel {
+    debug_assert_eq!(node.as_rule(), Rule::label_scoped);
     let span = node.span();
     let mut children = node.into_children();
-    let label = visit_label(children.consume_expected(Rule::label));
-    children.fold(TypeDeclaration::new(label, span), |type_declaration, constraint| {
-        let constraint = constraint.into_child().unwrap();
-        match constraint.as_rule() {
-            Rule::sub_declaration => type_declaration.set_sub(visit_sub_declaration(constraint)),
-            Rule::value_type_declaration => type_declaration.set_value_type(visit_value_type_declaration(constraint)),
-            Rule::owns_declaration => type_declaration.add_owns(visit_owns_declaration(constraint)),
-            Rule::relates_declaration => type_declaration.add_relates(visit_relates_declaration(constraint)),
-            Rule::plays_declaration => type_declaration.add_plays(visit_plays_declaration(constraint)),
-            _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: constraint.to_string() }),
-        }
-    })
+    let scope = visit_label(children.consume_expected(Rule::label));
+    let name = visit_label(children.consume_expected(Rule::label));
+    debug_assert_eq!(children.try_consume_any(), None);
+    ScopedLabel::new(span, scope, name)
 }
 
-fn visit_plays_declaration(node: Node<'_>) -> PlaysDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::plays_declaration);
-    let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::PLAYS);
-
-    let played_label = children.consume_any();
-    let label = visit_label(played_label);
-    let played = match children.try_consume_expected(Rule::AS) {
-        None => Played::new(label, None),
-        Some(_) => Played::new(label, Some(visit_label(children.consume_expected(Rule::label)))),
-    };
-
-    debug_assert!(children.try_consume_any().is_none());
-    PlaysDeclaration::new(played, span)
+fn visit_identifier(node: Node<'_>) -> Identifier {
+    debug_assert_eq!(node.as_rule(), Rule::identifier);
+    Identifier::new(node.span(), node.as_str().to_owned())
 }
 
-fn visit_relates_declaration(node: Node<'_>) -> RelatesDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::relates_declaration);
+fn visit_label_list(node: Node<'_>) -> List {
+    debug_assert_eq!(node.as_rule(), Rule::label_list);
     let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::RELATES);
-
-    let related_label = children.consume_any();
-    let related = match related_label.as_rule() {
-        Rule::list_label => Related::List(visit_list_label(related_label)),
-        Rule::label => {
-            let label = visit_label(related_label);
-            match children.try_consume_expected(Rule::AS) {
-                None => Related::Attribute(label, None),
-                Some(_) => Related::Attribute(label, Some(visit_label(children.consume_expected(Rule::label)))),
-            }
-        }
-        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: related_label.to_string() }),
-    };
-
-    let annotations_relates = visit_annotations_relates(children.consume_expected(Rule::annotations_relates));
-    debug_assert!(children.try_consume_any().is_none());
-    RelatesDeclaration::new(related, annotations_relates, span)
+    let inner = Type::Label(visit_label(node.into_child()));
+    List::new(span, inner)
 }
 
-fn visit_annotations_relates(node: Node<'_>) -> Vec<AnnotationRelates> {
-    vec![] // FIXME
+fn visit_var(node: Node<'_>) -> Variable {
+    debug_assert_eq!(node.as_rule(), Rule::var);
+    let span = node.span();
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::VAR_ANONYMOUS => Variable::Anonymous(span),
+        Rule::var_named => visit_var_named(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
 }
 
-fn visit_owns_declaration(node: Node<'_>) -> OwnsDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::owns_declaration);
+fn visit_var_named(node: Node<'_>) -> Variable {
+    debug_assert_eq!(node.as_rule(), Rule::var_named);
     let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::OWNS);
-
-    let owned_label = children.consume_any();
-    let owned = match owned_label.as_rule() {
-        Rule::list_label => Owned::List(visit_list_label(owned_label)),
-        Rule::label => {
-            let label = visit_label(owned_label);
-            match children.try_consume_expected(Rule::AS) {
-                None => Owned::Attribute(label, None),
-                Some(_) => Owned::Attribute(label, Some(visit_label(children.consume_expected(Rule::label)))),
-            }
-        }
-        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: owned_label.to_string() }),
-    };
-
-    let annotations_owns = visit_annotations_owns(children.consume_expected(Rule::annotations_owns));
-    debug_assert!(children.try_consume_any().is_none());
-    OwnsDeclaration::new(owned, annotations_owns, span)
+    Variable::Named(span, visit_identifier_var(node.into_child()))
 }
 
-fn visit_list_label(node: Node<'_>) -> Label {
-    debug_assert_eq!(node.as_rule(), Rule::list_label);
-    let mut children = node.into_children();
-    let label = children.consume_expected(Rule::LABEL);
-    debug_assert!(children.try_consume_any().is_none());
-    Label::new_unscoped(label.as_str(), label.span())
+fn visit_identifier_var(node: Node<'_>) -> Identifier {
+    debug_assert_eq!(node.as_rule(), Rule::identifier_var);
+    Identifier::new(node.span(), node.as_str().to_owned())
 }
 
-fn visit_annotations_owns(node: Node<'_>) -> Vec<AnnotationOwns> {
-    debug_assert_eq!(node.as_rule(), Rule::annotations_owns);
-    let children = node.into_children();
-    children
-        .map(|anno| {
-            let span = anno.span();
-            match anno.as_rule() {
-                Rule::annotation_card => {
-                    let mut children = anno.into_children();
-                    children.skip_expected(Rule::ANNOTATION_CARD);
-                    let lower = children.consume_expected(Rule::ANNOTATION_CARD_LOWER).as_str().parse().unwrap();
-                    let upper = children.consume_expected(Rule::ANNOTATION_CARD_UPPER).as_str().parse().ok();
-                    AnnotationOwns::Cardinality(lower, upper, span)
-                }
-                Rule::ANNOTATION_DISTINCT => AnnotationOwns::Distinct(span),
-                Rule::ANNOTATION_KEY => AnnotationOwns::Key(span),
-                Rule::ANNOTATION_UNIQUE => AnnotationOwns::Unique(span),
-                _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: anno.to_string() }),
-            }
-        })
-        .collect()
+fn visit_vars(node: Node<'_>) -> Vec<Variable> {
+    debug_assert_eq!(node.as_rule(), Rule::vars);
+    node.into_children().map(visit_var).collect()
 }
 
-fn visit_value_type_declaration(node: Node<'_>) -> ValueTypeDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::value_type_declaration);
+fn visit_var_list(node: Node<'_>) -> List {
+    debug_assert_eq!(node.as_rule(), Rule::var_list);
     let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::VALUE);
-    let value_type_node = children.consume_any();
-    let (value_type, annotations_value_type) = match value_type_node.as_rule() {
-        Rule::label => (visit_label(value_type_node).to_string(), Vec::new()),
-        Rule::value_type_primitive => (
-            value_type_node.as_str().to_owned(),
-            visit_annotations_value(children.consume_expected(Rule::annotations_value)),
-        ),
-        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: value_type_node.to_string() }),
-    };
-    debug_assert!(children.try_consume_any().is_none());
-    ValueTypeDeclaration::new(value_type, annotations_value_type, span)
+    let inner = visit_var(node.into_child());
+    List::new(span, Type::Variable(inner))
 }
 
-fn visit_annotations_value(node: Node<'_>) -> Vec<AnnotationValueType> {
-    node.into_children()
-        .map(|anno| match anno.as_rule() {
-            Rule::annotation_regex => visit_annotation_regex(anno),
-            Rule::annotation_values => visit_annotation_values(anno),
-            _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: anno.to_string() }),
-        })
-        .collect()
+fn visit_value_type(node: Node<'_>) -> Type {
+    debug_assert_eq!(node.as_rule(), Rule::value_type);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::value_type_primitive => Type::BuiltinValue(visit_value_type_primitive(child)),
+        Rule::identifier => Type::Label(Label::Identifier(visit_identifier(child))),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
 }
 
-fn visit_annotation_values(node: Node<'_>) -> AnnotationValueType {
+fn visit_value_type_optional(node: Node<'_>) -> Optional {
+    debug_assert_eq!(node.as_rule(), Rule::value_type_optional);
     let span = node.span();
-    let values = node.into_children().skip_expected(Rule::ANNOTATION_VALUES).map(|v| v.as_str().to_owned()).collect();
-    AnnotationValueType::Values(values, span)
+    let inner = visit_value_type(node.into_child());
+    Optional::new(span, inner)
 }
 
-fn visit_annotation_regex(node: Node<'_>) -> AnnotationValueType {
+fn visit_value_type_list(node: Node<'_>) -> List {
+    debug_assert_eq!(node.as_rule(), Rule::value_type_list);
     let span = node.span();
-    let mut children = node.into_children();
-    children.consume_expected(Rule::ANNOTATION_REGEX);
-    let regex = children.consume_expected(Rule::QUOTED_STRING).as_str().to_owned();
-    AnnotationValueType::Regex(regex, span)
+    let inner = visit_value_type(node.into_child());
+    List::new(span, inner)
 }
 
-fn visit_sub_declaration(node: Node<'_>) -> SubDeclaration {
-    debug_assert_eq!(node.as_rule(), Rule::sub_declaration);
+fn visit_value_type_primitive(node: Node<'_>) -> BuiltinValueType {
+    debug_assert_eq!(node.as_rule(), Rule::value_type_primitive);
     let span = node.span();
-    let mut children = node.into_children();
-    children.skip_expected(Rule::SUB);
-    let supertype_label = visit_label(children.consume_expected(Rule::label));
-    let annotations_sub = visit_annotations_sub(children.consume_expected(Rule::annotations_sub));
-    debug_assert!(children.try_consume_any().is_none());
-    SubDeclaration::new(supertype_label, annotations_sub, span)
+    let child = node.into_child();
+    let token = match child.as_rule() {
+        Rule::BOOLEAN => token::ValueType::Boolean,
+        Rule::DATE => token::ValueType::Date,
+        Rule::DATETIME => token::ValueType::DateTime,
+        Rule::DATETIME_TZ => token::ValueType::DateTimeTZ,
+        Rule::DECIMAL => token::ValueType::Decimal,
+        Rule::DOUBLE => token::ValueType::Double,
+        Rule::DURATION => token::ValueType::Duration,
+        Rule::LONG => token::ValueType::Long,
+        Rule::STRING => token::ValueType::String,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+    BuiltinValueType::new(span, token)
 }
 
-fn visit_annotations_sub(node: Node<'_>) -> Vec<AnnotationSub> {
-    node.into_children()
-        .map(|anno| match anno.as_rule() {
-            Rule::ANNOTATION_ABSTRACT => AnnotationSub::Abstract(anno.span()),
-            Rule::ANNOTATION_CASCADE => AnnotationSub::Cascade(anno.span()),
-            Rule::ANNOTATION_INDEPENDENT => AnnotationSub::Independent(anno.span()),
-            _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: anno.to_string() }),
-        })
-        .collect()
+fn visit_value_literal(node: Node<'_>) -> Literal {
+    debug_assert_eq!(node.as_rule(), Rule::value_literal);
+    Literal::new(node.span(), None, node.as_str().to_owned()) // TODO visit to get category
+}
+
+fn visit_quoted_string_literal(node: Node<'_>) -> Literal {
+    debug_assert_eq!(node.as_rule(), Rule::quoted_string_literal);
+    Literal::new(node.span(), Some(Tag::String), node.as_str().to_owned())
 }
 
-fn visit_label(label: Node<'_>) -> Label {
-    Label::new_unscoped(label.as_str(), label.span())
+fn visit_integer_literal(node: Node<'_>) -> Literal {
+    debug_assert_eq!(node.as_rule(), Rule::integer_literal);
+    Literal::new(node.span(), Some(Tag::Integral), node.as_str().to_owned())
 }
diff --git a/rust/parser/pipeline.rs b/rust/parser/pipeline.rs
new file mode 100644
index 000000000..7204e46f3
--- /dev/null
+++ b/rust/parser/pipeline.rs
@@ -0,0 +1,416 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use itertools::Itertools;
+
+use super::{
+    define::function::visit_definition_function,
+    statement::{
+        thing::{visit_relation, visit_statement_thing},
+        visit_statement,
+    },
+    visit_integer_literal, visit_label, visit_var, visit_vars, IntoChildNodes, Node, Rule, RuleMatcher,
+};
+use crate::{
+    common::{
+        error::TypeQLError,
+        token::{Aggregate, Order},
+        Spanned,
+    },
+    pattern::{Conjunction, Disjunction, Negation, Pattern, Try},
+    query::{
+        pipeline::{
+            stage::{
+                delete::{Deletable, DeletableKind},
+                fetch::{Projection, ProjectionAttribute, ProjectionKeyLabel, ProjectionKeyVar},
+                modifier::{Filter, Limit, Offset, OrderedVariable, Sort},
+                reduce::{Check, Count, First, ReduceValue, Stat},
+                Delete, Fetch, Insert, Match, Modifier, Put, Reduce, Stage, Update,
+            },
+            Preamble,
+        },
+        Pipeline,
+    },
+    type_::Type,
+};
+
+pub(super) fn visit_query_pipeline(node: Node<'_>) -> Pipeline {
+    debug_assert_eq!(node.as_rule(), Rule::query_pipeline);
+    let span = node.span();
+    let mut children = node.into_children();
+
+    let preambles = children.take_while_ref(|child| child.as_rule() == Rule::preamble).map(visit_preamble).collect();
+    let mut stages =
+        children.take_while_ref(|child| child.as_rule() == Rule::query_stage).map(visit_query_stage).collect_vec();
+    stages.extend(children.try_consume_expected(Rule::query_stage_terminal).map(visit_query_stage_terminal));
+
+    debug_assert_eq!(children.try_consume_any(), None);
+
+    Pipeline::new(span, preambles, stages)
+}
+
+fn visit_preamble(node: Node<'_>) -> Preamble {
+    debug_assert_eq!(node.as_rule(), Rule::preamble);
+    let span = node.span();
+    let mut children = node.into_children();
+    let patterns =
+        visit_definition_function(children.skip_expected(Rule::WITH).consume_expected(Rule::definition_function));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Preamble::new(span, patterns)
+}
+
+fn visit_query_stage(node: Node<'_>) -> Stage {
+    debug_assert_eq!(node.as_rule(), Rule::query_stage);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::stage_match => Stage::Match(visit_stage_match(child)),
+        Rule::stage_insert => Stage::Insert(visit_stage_insert(child)),
+        Rule::stage_put => Stage::Put(visit_stage_put(child)),
+        Rule::stage_update => Stage::Update(visit_stage_update(child)),
+        Rule::stage_delete => Stage::Delete(visit_stage_delete(child)),
+        Rule::stage_modifier => Stage::Modifier(visit_stage_modifier(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_query_stage_terminal(node: Node<'_>) -> Stage {
+    debug_assert_eq!(node.as_rule(), Rule::query_stage_terminal);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::stage_fetch => Stage::Fetch(visit_stage_fetch(child)),
+        Rule::stage_reduce => Stage::Reduce(visit_stage_reduce(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+pub(super) fn visit_stage_match(node: Node<'_>) -> Match {
+    debug_assert_eq!(node.as_rule(), Rule::stage_match);
+    let span = node.span();
+    let mut children = node.into_children();
+    let patterns = visit_patterns(children.skip_expected(Rule::MATCH).consume_expected(Rule::patterns));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Match::new(span, patterns)
+}
+
+pub(super) fn visit_patterns(node: Node<'_>) -> Vec<Pattern> {
+    debug_assert_eq!(node.as_rule(), Rule::patterns);
+    node.into_children().map(visit_pattern).collect()
+}
+
+pub(super) fn visit_pattern(node: Node<'_>) -> Pattern {
+    debug_assert_eq!(node.as_rule(), Rule::pattern);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::pattern_conjunction => Pattern::Conjunction(visit_pattern_conjunction(child)),
+        Rule::pattern_disjunction => Pattern::Disjunction(visit_pattern_disjunction(child)),
+        Rule::pattern_negation => Pattern::Negation(visit_pattern_negation(child)),
+        Rule::pattern_try => Pattern::Try(visit_pattern_try(child)),
+        Rule::statement => Pattern::Statement(visit_statement(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_pattern_conjunction(node: Node<'_>) -> Conjunction {
+    debug_assert_eq!(node.as_rule(), Rule::pattern_conjunction);
+    let span = node.span();
+    Conjunction::new(span, visit_patterns(node.into_child()))
+}
+
+fn visit_pattern_disjunction(node: Node<'_>) -> Disjunction {
+    debug_assert_eq!(node.as_rule(), Rule::pattern_disjunction);
+    let span = node.span();
+    let mut branches = Vec::new();
+    let mut children = node.into_children();
+    while let Some(branch) = children.try_consume_expected(Rule::patterns) {
+        branches.push(visit_patterns(branch));
+        children.try_consume_expected(Rule::OR);
+    }
+    debug_assert_eq!(children.try_consume_any(), None);
+    Disjunction::new(span, branches)
+}
+
+fn visit_pattern_negation(node: Node<'_>) -> Negation {
+    debug_assert_eq!(node.as_rule(), Rule::pattern_negation);
+    let span = node.span();
+    let mut children = node.into_children();
+    let patterns = children.skip_expected(Rule::NOT).consume_expected(Rule::patterns);
+    debug_assert_eq!(children.try_consume_any(), None);
+    Negation::new(span, visit_patterns(patterns))
+}
+
+fn visit_pattern_try(node: Node<'_>) -> Try {
+    debug_assert_eq!(node.as_rule(), Rule::pattern_try);
+    let span = node.span();
+    let mut children = node.into_children();
+    let patterns = children.skip_expected(Rule::TRY).consume_expected(Rule::patterns);
+    debug_assert_eq!(children.try_consume_any(), None);
+    Try::new(span, visit_patterns(patterns))
+}
+
+fn visit_stage_insert(node: Node<'_>) -> Insert {
+    debug_assert_eq!(node.as_rule(), Rule::stage_insert);
+    let span = node.span();
+    let statement_things = node.into_children().skip_expected(Rule::INSERT).map(visit_statement_thing).collect();
+    Insert::new(span, statement_things)
+}
+
+fn visit_stage_put(node: Node<'_>) -> Put {
+    debug_assert_eq!(node.as_rule(), Rule::stage_put);
+    let span = node.span();
+    let statement_things = node.into_children().skip_expected(Rule::PUT).map(visit_statement_thing).collect();
+    Put::new(span, statement_things)
+}
+
+fn visit_stage_update(node: Node<'_>) -> Update {
+    debug_assert_eq!(node.as_rule(), Rule::stage_update);
+    let span = node.span();
+    let statement_things = node.into_children().skip_expected(Rule::UPDATE).map(visit_statement_thing).collect();
+    Update::new(span, statement_things)
+}
+
+fn visit_stage_delete(node: Node<'_>) -> Delete {
+    debug_assert_eq!(node.as_rule(), Rule::stage_delete);
+    let span = node.span();
+    let deletables = node.into_children().skip_expected(Rule::DELETE).map(visit_statement_deletable).collect();
+    Delete::new(span, deletables)
+}
+
+fn visit_statement_deletable(node: Node<'_>) -> Deletable {
+    debug_assert_eq!(node.as_rule(), Rule::statement_deletable);
+    let span = node.span();
+    let mut children = node.into_children();
+    let kind = match children.peek_rule().unwrap() {
+        Rule::var if children.len() == 1 => {
+            DeletableKind::Concept { variable: visit_var(children.consume_expected(Rule::var)) }
+        }
+        Rule::HAS | Rule::var => {
+            children.try_consume_expected(Rule::HAS);
+            let attribute = visit_var(children.consume_expected(Rule::var));
+            children.skip_expected(Rule::OF);
+            let owner = visit_var(children.consume_expected(Rule::var));
+            DeletableKind::Has { attribute, owner }
+        }
+        Rule::LINKS | Rule::relation => {
+            children.try_consume_expected(Rule::LINKS);
+            let players = visit_relation(children.consume_expected(Rule::relation));
+            children.skip_expected(Rule::OF);
+            let relation = visit_var(children.consume_expected(Rule::var));
+            DeletableKind::Links { players, relation }
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: children.to_string() }),
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    Deletable::new(span, kind)
+}
+
+fn visit_stage_fetch(node: Node<'_>) -> Fetch {
+    debug_assert_eq!(node.as_rule(), Rule::stage_fetch);
+    let span = node.span();
+    let mut children = node.into_children();
+    let projections = visit_projections(children.skip_expected(Rule::FETCH).consume_expected(Rule::projections));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Fetch::new(span, projections)
+}
+
+fn visit_projections(node: Node<'_>) -> Vec<Projection> {
+    debug_assert_eq!(node.as_rule(), Rule::projections);
+    node.into_children().map(visit_projection).collect()
+}
+
+fn visit_projection(node: Node<'_>) -> Projection {
+    debug_assert_eq!(node.as_rule(), Rule::projection);
+    let mut children = node.into_children();
+    let child = children.consume_any();
+    match child.as_rule() {
+        Rule::projection_key_var => {
+            let key_var = visit_projection_key_var(child);
+            let node = children.try_consume_any();
+            if let Some(n) = node {
+                let attrs = visit_projection_attributes(n);
+                Projection::Attribute(key_var, attrs)
+            } else {
+                Projection::Variable(key_var)
+            }
+        }
+        Rule::projection_key_label => {
+            let key_label = visit_projection_key_label(child);
+            let subquery = visit_projection_subquery(children.consume_expected(Rule::projection_subquery));
+            Projection::Subquery(key_label, subquery)
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_projection_key_label(node: Node<'_>) -> ProjectionKeyLabel {
+    debug_assert_eq!(node.as_rule(), Rule::projection_key_label);
+    let mut children = node.into_children();
+    let child = children.consume_any();
+    match child.as_rule() {
+        Rule::quoted_string_literal => ProjectionKeyLabel { label: child.as_str().to_owned() },
+        Rule::label => ProjectionKeyLabel { label: child.as_str().to_owned() },
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_projection_subquery(node: Node<'_>) -> Pipeline {
+    debug_assert_eq!(node.as_rule(), Rule::projection_subquery);
+    visit_query_pipeline(node.into_child())
+}
+
+fn visit_projection_attributes(node: Node<'_>) -> Vec<ProjectionAttribute> {
+    debug_assert_eq!(node.as_rule(), Rule::projection_attributes);
+    node.into_children().map(visit_projection_attribute).collect()
+}
+
+fn visit_projection_attribute(node: Node<'_>) -> ProjectionAttribute {
+    debug_assert_eq!(node.as_rule(), Rule::projection_attribute);
+    let mut children = node.into_children();
+    let attribute_label = Type::Label(visit_label(children.consume_expected(Rule::label)));
+    let label = children.try_consume_expected(Rule::projection_key_as_label).map(visit_projection_key_as_label);
+    debug_assert!(children.try_consume_any().is_none());
+    ProjectionAttribute::new(attribute_label, label)
+}
+
+fn visit_projection_key_var(node: Node<'_>) -> ProjectionKeyVar {
+    debug_assert_eq!(node.as_rule(), Rule::projection_key_var);
+    let mut children = node.into_children();
+    let variable = visit_var(children.consume_expected(Rule::var));
+    let label = children.try_consume_expected(Rule::projection_key_as_label).map(visit_projection_key_as_label);
+    debug_assert!(children.try_consume_any().is_none());
+    ProjectionKeyVar::new(variable, label)
+}
+
+fn visit_projection_key_as_label(node: Node<'_>) -> ProjectionKeyLabel {
+    debug_assert_eq!(node.as_rule(), Rule::projection_key_as_label);
+    let mut children = node.into_children();
+    children.skip_expected(Rule::AS);
+    let label = visit_projection_key_label(children.consume_expected(Rule::projection_key_label));
+    debug_assert!(children.try_consume_any().is_none());
+    label
+}
+fn visit_stage_reduce(node: Node<'_>) -> Reduce {
+    debug_assert_eq!(node.as_rule(), Rule::stage_reduce);
+    visit_reduce(node.into_child())
+}
+
+pub(super) fn visit_reduce(node: Node<'_>) -> Reduce {
+    debug_assert_eq!(node.as_rule(), Rule::reduce);
+    let mut children = node.into_children();
+    let reduce = match children.peek_rule().unwrap() {
+        Rule::CHECK => Reduce::Check(Check::new(children.consume_expected(Rule::CHECK).span())),
+        Rule::reduce_first => Reduce::First(visit_reduce_first(children.consume_expected(Rule::reduce_first))),
+        Rule::reduce_value => Reduce::Value(children.by_ref().map(visit_reduce_value).collect()),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: children.to_string() }),
+    };
+    debug_assert!(children.try_consume_any().is_none());
+    reduce
+}
+
+fn visit_reduce_first(node: Node<'_>) -> First {
+    debug_assert_eq!(node.as_rule(), Rule::reduce_first);
+    let span = node.span();
+    let mut children = node.into_children();
+    let variables = visit_vars(children.skip_expected(Rule::FIRST).consume_expected(Rule::vars));
+    First::new(span, variables)
+}
+
+fn visit_reduce_value(node: Node<'_>) -> ReduceValue {
+    debug_assert_eq!(node.as_rule(), Rule::reduce_value);
+    let span = node.span();
+    let mut children = node.into_children();
+    let keyword = children.consume_any();
+    match keyword.as_rule() {
+        Rule::COUNT => ReduceValue::Count(Count::new(
+            span,
+            children.try_consume_expected(Rule::vars).map(visit_vars).unwrap_or_default(),
+        )),
+        Rule::MAX => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Max, visit_var(children.consume_expected(Rule::var))))
+        }
+        Rule::MIN => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Min, visit_var(children.consume_expected(Rule::var))))
+        }
+        Rule::MEAN => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Mean, visit_var(children.consume_expected(Rule::var))))
+        }
+        Rule::MEDIAN => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Median, visit_var(children.consume_expected(Rule::var))))
+        }
+        Rule::STD => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Std, visit_var(children.consume_expected(Rule::var))))
+        }
+        Rule::SUM => {
+            ReduceValue::Stat(Stat::new(span, Aggregate::Sum, visit_var(children.consume_expected(Rule::var))))
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: keyword.to_string() }),
+    }
+}
+
+pub(super) fn visit_stage_modifier(node: Node<'_>) -> Modifier {
+    debug_assert_eq!(node.as_rule(), Rule::stage_modifier);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::stage_filter => Modifier::Filter(visit_stage_filter(child)),
+        Rule::stage_sort => Modifier::Sort(visit_stage_sort(child)),
+        Rule::stage_offset => Modifier::Offset(visit_stage_offset(child)),
+        Rule::stage_limit => Modifier::Limit(visit_stage_limit(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_stage_filter(node: Node<'_>) -> Filter {
+    debug_assert_eq!(node.as_rule(), Rule::stage_filter);
+    let span = node.span();
+    let mut children = node.into_children();
+    let variables = visit_vars(children.skip_expected(Rule::FILTER).consume_expected(Rule::vars));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Filter::new(span, variables)
+}
+
+fn visit_stage_sort(node: Node<'_>) -> Sort {
+    debug_assert_eq!(node.as_rule(), Rule::stage_sort);
+    let span = node.span();
+    let ordered_variables = node.into_children().skip_expected(Rule::SORT).map(visit_var_order).collect();
+    Sort::new(span, ordered_variables)
+}
+
+fn visit_var_order(node: Node<'_>) -> OrderedVariable {
+    debug_assert_eq!(node.as_rule(), Rule::var_order);
+    let span = node.span();
+    let mut children = node.into_children();
+    let variable = visit_var(children.consume_expected(Rule::var));
+    let ordering = children.try_consume_expected(Rule::ORDER).map(visit_order);
+    debug_assert_eq!(children.try_consume_any(), None);
+    OrderedVariable::new(span, variable, ordering)
+}
+
+fn visit_order(node: Node<'_>) -> Order {
+    debug_assert_eq!(node.as_rule(), Rule::ORDER);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::ASC => Order::Asc,
+        Rule::DESC => Order::Desc,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_stage_offset(node: Node<'_>) -> Offset {
+    debug_assert_eq!(node.as_rule(), Rule::stage_offset);
+    let span = node.span();
+    let mut children = node.into_children();
+    let offset = visit_integer_literal(children.skip_expected(Rule::OFFSET).consume_expected(Rule::integer_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Offset::new(span, offset)
+}
+
+fn visit_stage_limit(node: Node<'_>) -> Limit {
+    debug_assert_eq!(node.as_rule(), Rule::stage_limit);
+    let span = node.span();
+    let mut children = node.into_children();
+    let limit = visit_integer_literal(children.skip_expected(Rule::LIMIT).consume_expected(Rule::integer_literal));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Limit::new(span, limit)
+}
diff --git a/rust/parser/redefine.rs b/rust/parser/redefine.rs
new file mode 100644
index 000000000..2bd33913a
--- /dev/null
+++ b/rust/parser/redefine.rs
@@ -0,0 +1,32 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::{IntoChildNodes, Node, Rule, RuleMatcher};
+use crate::{
+    common::Spanned,
+    parser::{define::type_::visit_type_capability, visit_identifier, visit_kind},
+    query::schema::Redefine,
+    schema::definable::{Definable, Type},
+    type_::Label,
+};
+
+pub(super) fn visit_query_redefine(node: Node<'_>) -> Redefine {
+    debug_assert_eq!(node.as_rule(), Rule::query_redefine);
+    let span = node.span();
+    let redefinables = node.into_children().skip_expected(Rule::REDEFINE).map(visit_redefinable).collect();
+    Redefine::new(span, redefinables)
+}
+
+fn visit_redefinable(node: Node<'_>) -> Definable {
+    debug_assert_eq!(node.as_rule(), Rule::redefinable);
+    let span = node.span();
+    let mut children = node.into_children();
+    let kind = children.try_consume_expected(Rule::kind).map(visit_kind);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    let capability = visit_type_capability(children.consume_expected(Rule::type_capability));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Definable::TypeDeclaration(Type::new(span, kind, Label::Identifier(ident), vec![capability]))
+}
diff --git a/rust/parser/statement/mod.rs b/rust/parser/statement/mod.rs
new file mode 100644
index 000000000..ba1653ca9
--- /dev/null
+++ b/rust/parser/statement/mod.rs
@@ -0,0 +1,97 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use self::{
+    single::visit_statement_single,
+    thing::{visit_statement_relation_anonymous, visit_statement_thing_var},
+};
+use super::{expression::visit_expression_value, IntoChildNodes, Node, Rule, RuleMatcher};
+use crate::{
+    common::{error::TypeQLError, token::Comparator, Spanned},
+    parser::{
+        statement::type_::visit_statement_type, visit_label, visit_label_list, visit_label_scoped, visit_var,
+        visit_var_list,
+    },
+    statement::{comparison::Comparison, Statement},
+    type_::{Type, TypeAny},
+};
+
+mod single;
+pub(super) mod thing;
+pub(super) mod type_;
+
+pub(super) fn visit_statement(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::statement_single => visit_statement_single(child),
+        Rule::statement_type => visit_statement_type(child),
+        Rule::statement_thing_var => visit_statement_thing_var(child),
+        Rule::statement_relation_anonymous => visit_statement_relation_anonymous(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_comparison(node: Node<'_>) -> Comparison {
+    debug_assert_eq!(node.as_rule(), Rule::comparison);
+    let span = node.span();
+    let mut children = node.into_children();
+    let comparator_node = children.consume_expected(Rule::comparator).into_child();
+    let comparator = match comparator_node.as_rule() {
+        Rule::EQ => Comparator::Eq,
+        Rule::NEQ => Comparator::Neq,
+        Rule::GTE => Comparator::Gte,
+        Rule::GT => Comparator::Gt,
+        Rule::LTE => Comparator::Lte,
+        Rule::LT => Comparator::Lt,
+        Rule::CONTAINS => Comparator::Contains,
+        Rule::LIKE => Comparator::Like,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: comparator_node.to_string() }),
+    };
+    let rhs = visit_expression_value(children.consume_expected(Rule::expression_value));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Comparison::new(span, comparator, rhs)
+}
+
+fn visit_type_ref_any(node: Node<'_>) -> Type {
+    debug_assert_eq!(node.as_rule(), Rule::type_ref_any);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::type_ref => visit_type_ref(child),
+        Rule::type_ref_scoped => visit_type_ref_scoped(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_type_ref(node: Node<'_>) -> Type {
+    debug_assert_eq!(node.as_rule(), Rule::type_ref);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var => Type::Variable(visit_var(child)),
+        Rule::label => Type::Label(visit_label(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_type_ref_scoped(node: Node<'_>) -> Type {
+    debug_assert_eq!(node.as_rule(), Rule::type_ref_scoped);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var => Type::Variable(visit_var(child)),
+        Rule::label_scoped => Type::ScopedLabel(visit_label_scoped(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_type_ref_list(node: Node<'_>) -> TypeAny {
+    debug_assert_eq!(node.as_rule(), Rule::type_ref_list);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var_list => TypeAny::List(visit_var_list(child)),
+        Rule::label_list => TypeAny::List(visit_label_list(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
diff --git a/rust/parser/statement/single.rs b/rust/parser/statement/single.rs
new file mode 100644
index 000000000..108650b50
--- /dev/null
+++ b/rust/parser/statement/single.rs
@@ -0,0 +1,114 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use itertools::Itertools;
+
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    parser::{
+        expression::{visit_expression, visit_expression_function, visit_expression_value},
+        statement::visit_comparison,
+        visit_label, visit_var, visit_vars, IntoChildNodes, Node, Rule, RuleMatcher,
+    },
+    statement::{
+        comparison::ComparisonStatement, Assignment, AssignmentPattern, DeconstructField, InStream, Is, Statement,
+        StructDeconstruct,
+    },
+    type_::Label,
+};
+
+pub(super) fn visit_statement_single(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_single);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::statement_is => Statement::Is(visit_statement_is(child)),
+        Rule::statement_in => Statement::InStream(visit_statement_in(child)),
+        Rule::statement_comparison => Statement::Comparison(visit_statement_comparison(child)),
+        Rule::statement_assignment => Statement::Assignment(visit_statement_assignment(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+pub fn visit_statement_assignment(node: Node<'_>) -> Assignment {
+    debug_assert_eq!(node.as_rule(), Rule::statement_assignment);
+    let span = node.span();
+    let mut children = node.into_children();
+    let lhs = visit_assignment_left(children.consume_expected(Rule::assignment_left));
+    children.skip_expected(Rule::ASSIGN);
+    let rhs = visit_expression(children.consume_expected(Rule::expression));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Assignment::new(span, lhs, rhs)
+}
+
+pub fn visit_assignment_left(node: Node<'_>) -> AssignmentPattern {
+    debug_assert_eq!(node.as_rule(), Rule::assignment_left);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::vars => AssignmentPattern::Variables(visit_vars(child)),
+        Rule::struct_destructor => AssignmentPattern::Deconstruct(visit_struct_destructor(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_struct_destructor(node: Node<'_>) -> StructDeconstruct {
+    debug_assert_eq!(node.as_rule(), Rule::struct_destructor);
+    let span = node.span();
+    let mut children = node.into_children();
+
+    let field_map = children
+        .by_ref()
+        .tuple_windows()
+        .map(|(key, value)| (visit_struct_key(key), visit_struct_destructor_value(value)))
+        .collect();
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    StructDeconstruct::new(span, field_map)
+}
+
+fn visit_struct_destructor_value(node: Node<'_>) -> DeconstructField {
+    debug_assert_eq!(node.as_rule(), Rule::struct_destructor_value);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var => DeconstructField::Variable(visit_var(child)),
+        Rule::struct_destructor => DeconstructField::Deconstruct(visit_struct_destructor(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_struct_key(node: Node<'_>) -> Label {
+    debug_assert_eq!(node.as_rule(), Rule::struct_key);
+    visit_label(node.into_child())
+}
+
+pub fn visit_statement_comparison(node: Node<'_>) -> ComparisonStatement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_comparison);
+    let span = node.span();
+    let mut children = node.into_children();
+    let lhs = visit_expression_value(children.consume_expected(Rule::expression_value));
+    let comparison = visit_comparison(children.consume_expected(Rule::comparison));
+    debug_assert_eq!(children.try_consume_any(), None);
+    ComparisonStatement::new(span, lhs, comparison)
+}
+
+pub fn visit_statement_is(node: Node<'_>) -> Is {
+    debug_assert_eq!(node.as_rule(), Rule::statement_is);
+    let span = node.span();
+    let mut children = node.into_children();
+    let lhs = visit_var(children.consume_expected(Rule::var));
+    children.skip_expected(Rule::IS);
+    let rhs = visit_var(children.consume_expected(Rule::var));
+    Is::new(span, lhs, rhs)
+}
+
+pub fn visit_statement_in(node: Node<'_>) -> InStream {
+    debug_assert_eq!(node.as_rule(), Rule::statement_in);
+    let span = node.span();
+    let mut children = node.into_children();
+    let lhs = visit_vars(children.consume_expected(Rule::vars));
+    children.skip_expected(Rule::IN);
+    let rhs = visit_expression_function(children.consume_expected(Rule::expression_function));
+    InStream::new(span, lhs, rhs)
+}
diff --git a/rust/parser/statement/thing.rs b/rust/parser/statement/thing.rs
new file mode 100644
index 000000000..ca604c8aa
--- /dev/null
+++ b/rust/parser/statement/thing.rs
@@ -0,0 +1,198 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::visit_comparison;
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    expression::Expression,
+    parser::{
+        expression::{visit_expression_list, visit_expression_struct, visit_expression_value},
+        statement::visit_type_ref,
+        visit_label_list, visit_value_literal, visit_var, visit_var_list, IntoChildNodes, Node, Rule, RuleMatcher,
+    },
+    statement::{
+        thing::{
+            isa::{Isa, IsaKind},
+            AttributeComparisonStatement, AttributeValueStatement, Constraint, Has, HasValue, Head, Iid, Links,
+            Relation, RolePlayer, Thing,
+        },
+        Statement,
+    },
+    type_::{Type, TypeAny},
+};
+
+pub(in crate::parser) fn visit_statement_thing(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_thing);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::statement_thing_var => visit_statement_thing_var(child),
+        Rule::statement_relation_anonymous => visit_statement_relation_anonymous(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+pub(super) fn visit_statement_thing_var(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_thing_var);
+    let span = node.span();
+    let mut children = node.into_children();
+    let var = visit_var(children.consume_expected(Rule::var));
+    match children.peek_rule().unwrap() {
+        Rule::thing_constraint => {
+            Statement::Thing(Thing::new(span, Head::Variable(var), children.map(visit_thing_constraint).collect()))
+        }
+        Rule::value_literal => {
+            let value = visit_value_literal(children.consume_expected(Rule::value_literal));
+            let isa = visit_isa_constraint(children.consume_expected(Rule::isa_constraint));
+            debug_assert_eq!(children.try_consume_any(), None);
+            Statement::AttributeValue(AttributeValueStatement::new(span, Some(Type::Variable(var)), value, isa))
+        }
+        Rule::expression_struct => {
+            let value = visit_expression_struct(children.consume_expected(Rule::expression_struct));
+            let isa = visit_isa_constraint(children.consume_expected(Rule::isa_constraint));
+            debug_assert_eq!(children.try_consume_any(), None);
+            Statement::AttributeValue(AttributeValueStatement::new(span, Some(Type::Variable(var)), value, isa))
+        }
+        Rule::comparison => {
+            let comparison = visit_comparison(children.consume_expected(Rule::comparison));
+            let isa = visit_isa_constraint(children.consume_expected(Rule::isa_constraint));
+            debug_assert_eq!(children.try_consume_any(), None);
+            Statement::AttributeComparison(AttributeComparisonStatement::new(span, var, comparison, isa))
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: children.to_string() }),
+    }
+}
+
+pub(super) fn visit_statement_relation_anonymous(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_relation_anonymous);
+    let span = node.span();
+    let mut children = node.into_children();
+    let head = Head::Relation(visit_relation(children.consume_expected(Rule::relation)));
+    let constraints = children.map(visit_thing_constraint).collect();
+    Statement::Thing(Thing::new(span, head, constraints))
+}
+
+fn visit_thing_constraint(node: Node<'_>) -> Constraint {
+    debug_assert_eq!(node.as_rule(), Rule::thing_constraint);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::isa_constraint => Constraint::Isa(visit_isa_constraint(child)),
+        Rule::iid_constraint => Constraint::Iid(visit_iid_constraint(child)),
+        Rule::has_constraint => Constraint::Has(visit_has_constraint(child)),
+        Rule::links_constraint => Constraint::Links(visit_links_constraint(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_isa_constraint(node: Node<'_>) -> Isa {
+    debug_assert_eq!(node.as_rule(), Rule::isa_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    let kind = visit_isa_token(children.consume_expected(Rule::ISA_));
+    let type_ = visit_type_ref(children.consume_expected(Rule::type_ref));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Isa::new(span, kind, type_)
+}
+
+fn visit_isa_token(node: Node<'_>) -> IsaKind {
+    debug_assert_eq!(node.as_rule(), Rule::ISA_);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::ISA => IsaKind::Subtype,
+        Rule::ISAX => IsaKind::Exact,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_iid_constraint(node: Node<'_>) -> Iid {
+    debug_assert_eq!(node.as_rule(), Rule::iid_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    let iid = children.skip_expected(Rule::IID).consume_expected(Rule::iid_value).as_str().to_owned();
+    debug_assert_eq!(children.try_consume_any(), None);
+    Iid::new(span, iid)
+}
+
+fn visit_has_constraint(node: Node<'_>) -> Has {
+    debug_assert_eq!(node.as_rule(), Rule::has_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::HAS);
+    let child = children.consume_any();
+    let has = match child.as_rule() {
+        Rule::var => Has::new(span, None, HasValue::Variable(visit_var(child))),
+        Rule::type_ref => {
+            let type_ = Some(TypeAny::Type(visit_type_ref(child)));
+            let value_node = children.consume_any();
+            let value = match value_node.as_rule() {
+                Rule::comparison => HasValue::Comparison(visit_comparison(value_node)),
+                Rule::expression_value => match visit_expression_value(value_node) {
+                    Expression::Variable(variable) => HasValue::Variable(variable),
+                    expr => HasValue::Expression(expr),
+                },
+                _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: value_node.to_string() }),
+            };
+            Has::new(span, type_, value)
+        }
+        Rule::type_ref_list => {
+            let type_ = Some(visit_type_ref_list(child));
+            let value_node = children.consume_any();
+            let value = match value_node.as_rule() {
+                Rule::var => HasValue::Variable(visit_var(value_node)),
+                Rule::comparison => HasValue::Comparison(visit_comparison(value_node)),
+                Rule::expression_list => HasValue::Expression(visit_expression_list(value_node)),
+                _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: value_node.to_string() }),
+            };
+            Has::new(span, type_, value)
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    has
+}
+
+fn visit_links_constraint(node: Node<'_>) -> Links {
+    debug_assert_eq!(node.as_rule(), Rule::links_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    let relation = visit_relation(children.skip_expected(Rule::LINKS).consume_expected(Rule::relation));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Links::new(span, relation)
+}
+
+pub(in crate::parser) fn visit_relation(node: Node<'_>) -> Relation {
+    debug_assert_eq!(node.as_rule(), Rule::relation);
+    let span = node.span();
+    let role_players = node.into_children().map(visit_role_player).collect();
+    Relation::new(span, role_players)
+}
+
+fn visit_role_player(node: Node<'_>) -> RolePlayer {
+    debug_assert_eq!(node.as_rule(), Rule::role_player);
+    let mut children = node.into_children();
+    let child = children.consume_any();
+    let role_player = match child.as_rule() {
+        Rule::var => RolePlayer::Untyped(visit_var(child)),
+        Rule::type_ref => {
+            RolePlayer::Typed(TypeAny::Type(visit_type_ref(child)), visit_var(children.consume_expected(Rule::var)))
+        }
+        Rule::type_ref_list => {
+            RolePlayer::Typed(visit_type_ref_list(child), visit_var(children.consume_expected(Rule::var)))
+        }
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    role_player
+}
+
+fn visit_type_ref_list(node: Node<'_>) -> TypeAny {
+    debug_assert_eq!(node.as_rule(), Rule::type_ref_list);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::var_list => TypeAny::List(visit_var_list(child)),
+        Rule::label_list => TypeAny::List(visit_label_list(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
diff --git a/rust/parser/statement/type_.rs b/rust/parser/statement/type_.rs
new file mode 100644
index 000000000..83f0ac055
--- /dev/null
+++ b/rust/parser/statement/type_.rs
@@ -0,0 +1,159 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use crate::{
+    common::{error::TypeQLError, Spanned},
+    parser::{
+        annotation::visit_annotations,
+        statement::{visit_type_ref, visit_type_ref_any, visit_type_ref_list, visit_type_ref_scoped},
+        visit_label, visit_label_scoped, visit_value_type, IntoChildNodes, Node, Rule, RuleMatcher,
+    },
+    statement::{
+        type_::{Constraint, ConstraintBase, LabelConstraint, Owns, Plays, Relates, Sub, SubKind, ValueType},
+        Statement, Type,
+    },
+    type_::TypeAny,
+};
+
+pub(super) fn visit_statement_type(node: Node<'_>) -> Statement {
+    debug_assert_eq!(node.as_rule(), Rule::statement_type);
+    let span = node.span();
+    let mut children = node.into_children();
+    let type_ = visit_type_ref_any(children.consume_expected(Rule::type_ref_any));
+    let constraints = children.map(visit_type_constraint).collect();
+    Statement::Type(Type::new(span, type_, constraints))
+}
+
+fn visit_type_constraint(node: Node<'_>) -> Constraint {
+    debug_assert_eq!(node.as_rule(), Rule::type_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    let base = visit_type_constraint_base(children.consume_expected(Rule::type_constraint_base));
+    let annotations = children.try_consume_expected(Rule::annotations).map(visit_annotations).unwrap_or_default();
+    debug_assert_eq!(children.try_consume_any(), None);
+    Constraint::new(span, base, annotations)
+}
+
+fn visit_type_constraint_base(node: Node<'_>) -> ConstraintBase {
+    debug_assert_eq!(node.as_rule(), Rule::type_constraint_base);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::sub_constraint => ConstraintBase::Sub(visit_sub_constraint(child)),
+        Rule::value_type_constraint => ConstraintBase::ValueType(visit_value_type_constraint(child)),
+        Rule::label_constraint => ConstraintBase::Label(visit_label_constraint(child)),
+        Rule::owns_constraint => ConstraintBase::Owns(visit_owns_constraint(child)),
+        Rule::relates_constraint => ConstraintBase::Relates(visit_relates_constraint(child)),
+        Rule::plays_constraint => ConstraintBase::Plays(visit_plays_constraint(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_sub_constraint(node: Node<'_>) -> Sub {
+    debug_assert_eq!(node.as_rule(), Rule::sub_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    let kind = visit_sub_token(children.consume_expected(Rule::SUB_));
+    let supertype = visit_type_ref_any(children.consume_expected(Rule::type_ref_any));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Sub::new(span, kind, supertype)
+}
+
+fn visit_sub_token(node: Node<'_>) -> SubKind {
+    debug_assert_eq!(node.as_rule(), Rule::SUB_);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::SUB => SubKind::Transitive,
+        Rule::SUBX => SubKind::Direct,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_value_type_constraint(node: Node<'_>) -> ValueType {
+    debug_assert_eq!(node.as_rule(), Rule::value_type_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::VALUE);
+    let value_type = visit_value_type(children.consume_expected(Rule::value_type));
+    debug_assert_eq!(children.try_consume_any(), None);
+    ValueType::new(span, value_type)
+}
+
+fn visit_label_constraint(node: Node<'_>) -> LabelConstraint {
+    debug_assert_eq!(node.as_rule(), Rule::label_constraint);
+    let mut children = node.into_children();
+    children.skip_expected(Rule::LABEL);
+    let label = children.consume_any();
+    let label = match label.as_rule() {
+        Rule::label => LabelConstraint::Name(visit_label(label)),
+        Rule::label_scoped => LabelConstraint::Scoped(visit_label_scoped(label)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: label.to_string() }),
+    };
+    debug_assert_eq!(children.try_consume_any(), None);
+    label
+}
+
+fn visit_owns_constraint(node: Node<'_>) -> Owns {
+    debug_assert_eq!(node.as_rule(), Rule::owns_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::OWNS);
+
+    let child = children.consume_any();
+    let owned = match child.as_rule() {
+        Rule::type_ref => TypeAny::Type(visit_type_ref(child)),
+        Rule::type_ref_list => visit_type_ref_list(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_type_ref(children.consume_expected(Rule::type_ref)))
+    } else {
+        None
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Owns::new(span, owned, overridden)
+}
+
+fn visit_relates_constraint(node: Node<'_>) -> Relates {
+    debug_assert_eq!(node.as_rule(), Rule::relates_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::RELATES);
+
+    let child = children.consume_any();
+    let related = match child.as_rule() {
+        Rule::type_ref => TypeAny::Type(visit_type_ref(child)),
+        Rule::type_ref_list => visit_type_ref_list(child),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    };
+
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_type_ref(children.consume_expected(Rule::type_ref)))
+    } else {
+        None
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Relates::new(span, related, overridden)
+}
+
+fn visit_plays_constraint(node: Node<'_>) -> Plays {
+    debug_assert_eq!(node.as_rule(), Rule::plays_constraint);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::PLAYS);
+
+    let role = visit_type_ref_scoped(children.consume_expected(Rule::type_ref_scoped));
+    let overridden = if children.try_consume_expected(Rule::AS).is_some() {
+        Some(visit_type_ref(children.consume_expected(Rule::type_ref)))
+    } else {
+        None
+    };
+
+    debug_assert_eq!(children.try_consume_any(), None);
+    Plays::new(span, role, overridden)
+}
diff --git a/rust/parser/typeql.pest b/rust/parser/typeql.pest
index fb7afd145..e24d01183 100644
--- a/rust/parser/typeql.pest
+++ b/rust/parser/typeql.pest
@@ -5,90 +5,61 @@
  */
 
 eof_query = { SOI ~ query ~ EOI }
-eof_queries = { SOI ~ query+ ~ EOI }
-eof_pattern = { SOI ~ pattern ~ EOI }
-eof_patterns = { SOI ~ patterns ~ EOI }
-eof_definables = { SOI ~ definables ~ EOI }
-eof_statement = { SOI ~ statement ~ EOI }
 eof_label = { SOI ~ label ~ EOI }
 eof_definition_function = { SOI ~ definition_function ~ EOI }
+eof_definition_struct = { SOI ~ definition_struct ~ EOI }
 
 // TYPEQL QUERY LANGUAGE =======================================================
 
-query = { query_schema | query_data }
+query = { query_schema | query_pipeline }
 
-query_schema = { query_define | query_undefine }
-query_define = { DEFINE ~ definables }
-query_undefine = { UNDEFINE ~ definables }
-
-query_data = { query_stage+ }
-query_stage = { stage_match
-              | stage_insert
-              | stage_delete
-              | stage_put
-              | stage_fetch
-              | stage_reduce
-              | stage_modifier
-              }
+// QUERY PIPELINES =============================================================
 
-// query_get_group_agg = { query_get ~ clause_group ~ stage_reduce }
-// query_get_group = { query_get ~ clause_group }
-// query_get_aggregate = { query_get ~ stage_reduce }
-// query_get = { stage_match ~ clause_get ~ stage_modifier* }
-// clause_group = { GROUP ~ VAR ~ SEMICOLON } // TODO: eliminate?
+query_pipeline = { preamble* ~ query_stage+ ~ query_stage_terminal? }
 
-stage_match = { MATCH ~ patterns }
-stage_insert = { INSERT ~ statement_things }
-stage_delete = { DELETE ~ statement_things }
-stage_put = { DELETE ~ statement_things }
-stage_fetch = { FETCH ~ projections }
-stage_reduce = { reduce ~ SEMICOLON }
-reduce = { reduce_limited
-         | reduce_all ~ ( COMMA ~ reduce_all )* }
-reduce_all = { COUNT ~ PAREN_OPEN ~ VAR? ~ ( COMMA ~ VAR )* ~ PAREN_CLOSE
-             | MAX ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             | MEAN ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             | MEDIAN ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             | MIN ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             | STD ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             | SUM ~ PAREN_OPEN ~ VAR ~ PAREN_CLOSE
-             }
-reduce_limited = { CHECK | reduce_first }
-reduce_first = { FIRST ~ VAR ~ ( COMMA ~ VAR )* }
+preamble = { WITH ~ definition_function }
+query_stage = { stage_match | stage_insert | stage_put | stage_update | stage_delete | stage_modifier }
+query_stage_terminal = { stage_fetch | stage_reduce }
 
-// QUERY MODIFIERS =============================================================
+// stage_group = { GROUP ~ var ~ SEMICOLON } // TODO: eliminate?
 
-stage_modifier = { stage_filter ~ SEMICOLON | stage_sort ~ SEMICOLON | offset ~ SEMICOLON | stage_limit ~ SEMICOLON }
-stage_filter = { FILTER ~ VAR ~ ( COMMA ~ VAR )* }
-stage_limit = { LIMIT ~ LONG_VALUE }
-stage_sort = { SORT ~ var_order ~ ( COMMA ~ var_order )* }
-var_order = { VAR ~ ORDER_? }
-offset = { OFFSET ~ LONG_VALUE }
+stage_match = { MATCH ~ patterns }
 
-// FETCH QUERY =================================================================
+stage_insert = { INSERT ~ ( statement_thing ~ SEMICOLON )+ }
+stage_put = { PUT ~ ( statement_thing ~ SEMICOLON )+ }
+stage_update = { UPDATE ~ ( statement_thing ~ SEMICOLON )+ }
 
-projections = { ( projection ~ SEMICOLON )+ }
-projection = { projection_key_var ~ COLON ~ projection_attributes
-             | projection_key_var
-             | projection_key_label ~ COLON ~ CURLY_OPEN ~ projection_subquery ~ CURLY_CLOSE }
+stage_delete = { DELETE ~ ( statement_deletable ~ SEMICOLON )+ }
 
-projection_attributes = { projection_attribute ~ ( COMMA ~ projection_attribute )* }
-projection_attribute = { label ~ projection_key_as_label? }
+// QUERY MODIFIERS =============================================================
 
-// TODO: we have to validate this is a fetch or reduce ended pipeline
-projection_subquery = { query_data }
+stage_modifier = { stage_filter ~ SEMICOLON | stage_sort ~ SEMICOLON | stage_offset ~ SEMICOLON | stage_limit ~ SEMICOLON }
 
-projection_key_var = { VAR ~ projection_key_as_label? }
-projection_key_as_label = { AS ~ projection_key_label }
-projection_key_label = { QUOTED_STRING | label }
+stage_filter = { FILTER ~ vars }
+stage_sort = { SORT ~ var_order ~ ( COMMA ~ var_order )* }
+stage_offset = { OFFSET ~ integer_literal }
+stage_limit = { LIMIT ~ integer_literal }
 
-// SCHEMA QUERY ================================================================
+var_order = { var ~ ORDER? }
 
-definables = { definable+ }
-definable = { ( definition_type ~ SEMICOLON ) 
-            // | definition_function
-            // | definition_struct
-            }
+// REDUCE ======================================================================
+
+stage_reduce = { reduce ~ SEMICOLON }
+reduce = { CHECK
+         | reduce_first
+         | reduce_value ~ ( COMMA ~ reduce_value )*
+         }
+
+reduce_first = { FIRST ~ PAREN_OPEN ~ vars ~ PAREN_CLOSE }
+
+reduce_value = { COUNT ~ PAREN_OPEN ~ vars? ~ PAREN_CLOSE
+               | MAX ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               | MIN ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               | MEAN ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               | MEDIAN ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               | STD ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               | SUM ~ PAREN_OPEN ~ var ~ PAREN_CLOSE
+               }
 
 // QUERY PATTERNS ==============================================================
 
@@ -102,220 +73,329 @@ pattern_try = { TRY ~ CURLY_OPEN ~ patterns ~ CURLY_CLOSE }
 
 // STATEMENTS ==================================================================
 
-statement = { statement_single | statement_multi }
-
-// SINGLE STATEMENTS ===========================================================
-
-statement_single = { statement_is | statement_comparison | statement_assignment | statement_in }
-
-statement_is = { VAR ~ IS ~ VAR }
-statement_comparison = { expression_value ~ comparison }
-statement_assignment = { assignment_left ~ ASSIGN ~ expression_value_or_list }
-assignment_left = { VAR ~ ( COMMA ~ VAR )* | struct_destructor }
-statement_in = { VAR ~ ( COMMA ~ VAR )* ~ IN ~ expression_function }
-
-// MULTI STATEMENT =============================================================
-
-statement_multi = { statement_type | statement_thing }
+statement = { statement_type | statement_thing_var | statement_single | statement_relation_anonymous }
 
 // TYPE STATEMENTS =============================================================
 
 statement_type = { type_ref_any ~ type_constraint ~ ( COMMA ~ type_constraint )* }
-type_constraint = { SUB_ ~ type_ref_any ~ annotations_sub
-                  | VALUE ~ value_type_primitive ~ annotations_value
-                  | TYPE ~ label_any
-                  | owns_constraint
-                  | relates_constraint
-                  | plays_constraint
-                  }
-
-owns_constraint = { OWNS ~ type_ref_list ~ annotations_owns
-                  | OWNS ~ type_ref ~ ( AS ~ type_ref )? ~ annotations_owns
+type_constraint = { type_constraint_base ~ annotations? }
+type_constraint_base = { sub_constraint | value_type_constraint | label_constraint
+                       | owns_constraint | relates_constraint | plays_constraint
+                       }
+
+sub_constraint = { SUB_ ~ type_ref_any }
+value_type_constraint = { VALUE ~ value_type }
+label_constraint = { LABEL ~ ( label_scoped | label ) }
+owns_constraint = { OWNS ~ type_ref_list
+                  | OWNS ~ type_ref ~ ( AS ~ type_ref )?
                   }
-relates_constraint = { RELATES ~ type_ref_list ~ annotations_relates
-                     | RELATES ~ type_ref ~ ( AS ~ type_ref )? ~ annotations_relates
+relates_constraint = { RELATES ~ type_ref_list
+                     | RELATES ~ type_ref ~ ( AS ~ type_ref )?
                      }
 plays_constraint = { PLAYS ~ type_ref_scoped ~ ( AS ~ type_ref )? }
 
-annotations_sub = { ( ANNOTATION_ABSTRACT | ANNOTATION_CASCADE | ANNOTATION_INDEPENDENT )* }
-annotations_owns = { ( annotation_card | ANNOTATION_DISTINCT | ANNOTATION_KEY | ANNOTATION_UNIQUE )* }
-annotations_relates = { ( annotation_card | ANNOTATION_DISTINCT | ANNOTATION_CASCADE )* } // ( ANNOTATION_ABSTRACT )?
-annotations_value = { ( annotation_regex | annotation_values )* }
-
-annotation_card = { ANNOTATION_CARD ~ PAREN_OPEN ~ ANNOTATION_CARD_LOWER ~ COMMA ~ ANNOTATION_CARD_UPPER ~ PAREN_CLOSE }
-annotation_regex = { ANNOTATION_REGEX ~ PAREN_OPEN ~ QUOTED_STRING ~ PAREN_CLOSE }
-annotation_subkey = { ANNOTATION_SUBKEY ~ PAREN_OPEN ~ label ~ PAREN_CLOSE }
-annotation_values = { ANNOTATION_VALUES ~ PAREN_OPEN ~ value_primitive ~ ( COMMA ~ value_primitive )* ~ PAREN_CLOSE }
-
 // THING STATEMENTS ============================================================
 
-statement_things = { ( statement_thing ~ SEMICOLON )+ }
-statement_thing = { thing_start ~ ( COMMA ~ thing_constraint )* }
+statement_thing = { statement_thing_var | statement_relation_anonymous }
+statement_relation_anonymous = { relation ~ thing_constraint? ~ ( COMMA ~ thing_constraint )* }
 
-thing_start = { relation
-              | VAR ~ thing_constraint
-              | VAR ~ value_primitive ~ ISA_ ~ type_ref
-              | VAR ~ expression_struct ~ ISA_ ~ type_ref
-              | VAR ~ comparison ~ ISA_ ~ type_ref
-              }
+statement_thing_var = { var ~ thing_constraint ~ ( COMMA ~ thing_constraint )*
+                      | var ~ ( value_literal | expression_struct ) ~ isa_constraint
+                      | var ~ comparison ~ isa_constraint
+                      }
 
-thing_constraint = { ISA_ ~ type_ref
-                   | IID ~ IID_VALUE
-                   | relates_constraint
-                   | has_constraint
-                   }
+thing_constraint = { isa_constraint | iid_constraint | has_constraint | links_constraint }
 
-has_constraint = { HAS ~ ( VAR | label ~ ( expression_value | comparison ) )
-                 | HAS ~ type_ref_list ~ ( expression_list | comparison )
+isa_constraint = { ISA_ ~ type_ref }
+iid_constraint = { IID ~ iid_value }
+has_constraint = { HAS ~ type_ref_list ~ ( comparison | expression_list | var )
+                 | HAS ~ type_ref ~ ( comparison | expression_value | var )
+                 | HAS ~ var
                  }
-relation_constraint = { LINKS ~ relation }
+links_constraint = { LINKS ~ relation }
 
 relation = { PAREN_OPEN ~ role_player ~ ( COMMA ~ role_player )* ~ PAREN_CLOSE } // A list of role players in a Relations
-role_player = { type_ref_list ~ COLON ~ VAR
-              | type_ref ~ COLON ~ VAR
-              | VAR
+role_player = { type_ref_list ~ COLON ~ var
+              | type_ref ~ COLON ~ var
+              | var
               }
 
-// COMPARISON CONSTRUCTS =======================================================
+statement_deletable = { HAS? ~ var ~ OF ~ var
+                      | LINKS? ~ relation ~ OF ~ var
+                      | var
+                      }
+
+// SINGLE STATEMENTS ===========================================================
+
+statement_single = { statement_is | statement_comparison | statement_assignment | statement_in }
+
+statement_is = { var ~ IS ~ var }
+statement_comparison = { expression_value ~ comparison }
+statement_assignment = { assignment_left ~ ASSIGN ~ expression }
+assignment_left = { vars | struct_destructor }
+statement_in = { vars ~ IN ~ expression_function }
 
 comparison = { comparator ~ expression_value }
 comparator = { EQ | NEQ | GTE | GT | LTE | LT | CONTAINS | LIKE }
 
 // EXPRESSION CONSTRUCTS =======================================================
 
-expression_value_or_list = { expression_value | expression_list }
+expression = { expression_list | expression_value }
 
 expression_value = { expression_base ~ ( expression_operator ~ expression_base )* }
-expression_base = _{ VAR | value_primitive | expression_function | expression_parenthesis | expression_list_index }
+expression_base = { expression_list_index | expression_parenthesis | expression_function | value_literal | var }
 
-expression_operator = _{ POWER | MULTIPLY | DIVIDE | MODULO | ADD | SUBTRACT }
+expression_operator = _{ POWER | TIMES | DIVIDE | MODULO | PLUS | MINUS }
 expression_parenthesis = { PAREN_OPEN ~ expression_value ~ PAREN_CLOSE }
 
-expression_list_index = { VAR ~ list_index }
+expression_list_index = { var ~ list_index }
 list_index = { SQ_BRACKET_OPEN ~ expression_value ~ SQ_BRACKET_CLOSE }
 
 expression_function = { expression_function_name ~ PAREN_OPEN ~ expression_arguments? ~ PAREN_CLOSE }
-expression_function_name = { EXPR_FUNC_NAME | MAX | MIN | label }
-expression_arguments = { expression_value ~ ( COMMA ~ expression_value )* }
+expression_function_name = { builtin_func_name | identifier }
+expression_arguments = { expression ~ ( COMMA ~ expression )* }
 
-expression_list = { VAR | expression_list_new | expression_list_subrange }
+expression_list = { expression_list_subrange | expression_list_new }
 expression_list_new = { SQ_BRACKET_OPEN ~ expression_value ~ ( COMMA ~ expression_value )* ~ SQ_BRACKET_CLOSE }
-expression_list_subrange = { VAR ~ list_range }
-list_range = { SQ_BRACKET_OPEN ~ expression_value ~ COLON ~ expression_value ~ SQ_BRACKET_CLOSE }
+expression_list_subrange = { var ~ list_range }
+list_range = { SQ_BRACKET_OPEN ~ expression_value ~ DOUBLE_DOT ~ expression_value ~ SQ_BRACKET_CLOSE }
 
 expression_struct = { CURLY_OPEN ~ struct_key ~ COLON ~ struct_value ~ CURLY_CLOSE }
 struct_value = { expression_value | expression_struct }
 
+// FETCH QUERY =================================================================
+
+stage_fetch = { FETCH ~ projections }
+
+projections = { ( projection ~ SEMICOLON )+ }
+projection = { projection_key_var ~ COLON ~ projection_attributes
+             | projection_key_var
+             | projection_key_label ~ COLON ~ CURLY_OPEN ~ projection_subquery ~ CURLY_CLOSE
+             }
+
+projection_attributes = { projection_attribute ~ ( COMMA ~ projection_attribute )* }
+projection_attribute = { label ~ projection_key_as_label? }
+
+// TODO: we have to validate this is a fetch or reduce ended pipeline
+projection_subquery = { query_pipeline }
+
+projection_key_var = { var ~ projection_key_as_label? }
+projection_key_as_label = { AS ~ projection_key_label }
+projection_key_label = { quoted_string_literal | label }
+
+// SCHEMA QUERY ================================================================
+
+query_schema = { query_define | query_undefine | query_redefine }
+
+// DEFINE QUERY ================================================================
+
+query_define = { DEFINE ~ definables }
+
+definables = { definable+ }
+definable = { ( definition_type ~ SEMICOLON ) | definition_function | definition_struct }
+
 // TYPE DEFINITION =============================================================
 
-definition_type = { label ~ type_constraint_declaration ~ ( COMMA ~ type_constraint_declaration )* }
-type_constraint_declaration = { sub_declaration
-                              | value_type_declaration
-                              | owns_declaration
-                              | relates_declaration
-                              | plays_declaration
-                              }
-
-sub_declaration = { SUB ~ label ~ annotations_sub }
-value_type_declaration = { VALUE ~ ( value_type_primitive ~ annotations_value | label ) }
-owns_declaration = { OWNS ~ list_label ~ annotations_owns
-                   | OWNS ~ label ~ ( AS ~ label )? ~ annotations_owns
+definition_type = { kind? ~ identifier ~ type_capability ~ ( COMMA ~ type_capability )* }
+type_capability = { type_capability_base ~ annotations? }
+type_capability_base = { sub_declaration | value_type_declaration | alias_declaration
+                       | owns_declaration | plays_declaration | relates_declaration
+                       }
+
+sub_declaration = { SUB ~ label }
+alias_declaration = { ALIAS ~ label } 
+value_type_declaration = { VALUE ~ value_type }
+owns_declaration = { OWNS ~ label_list
+                   | OWNS ~ label ~ ( AS ~ label )?
                    }
-relates_declaration = { RELATES ~ list_label ~ annotations_relates
-                      | RELATES ~ label ~ ( AS ~ label )? ~ annotations_relates
-                      }
 plays_declaration = { PLAYS ~ label_scoped ~ ( AS ~ label )? }
+relates_declaration = { RELATES ~ label_list
+                      | RELATES ~ label ~ ( AS ~ label )?
+                      }
 
 // FUNCTION DEFINITION =========================================================
 
 definition_function = { FUN ~ function_signature ~ COLON ~ stage_match ~ stage_modifier* ~ return_statement }
 
-function_signature = { label ~ PAREN_OPEN ~ function_arguments ~ PAREN_CLOSE ~ ARROW ~ function_return }
+function_signature = { identifier ~ PAREN_OPEN ~ function_arguments ~ PAREN_CLOSE ~ ARROW ~ function_output }
 
-function_arguments = { ( function_argument ~ ( COMMA ~ function_argument ) * )? }
-function_argument = { VAR ~ COLON ~ label }
+function_arguments = { ( function_argument ~ ( COMMA ~ function_argument )* )? }
+function_argument = { var ~ COLON ~ label_arg }
 
-function_return = { function_return_stream | function_return_single }
-function_return_stream = { CURLY_OPEN ~ function_return_value ~ ( "," ~ function_return_value )* ~ CURLY_CLOSE }
-function_return_single = { function_return_value ~ ( "," ~ function_return_value )* }
-function_return_value = { function_return_value_optional | function_return_value_base }
-function_return_value_optional = { function_return_value_base ~ QUESTION }
-function_return_value_base = { label | list_label | value_type_primitive | list_value_type_primitive }
+function_output = { function_output_stream | function_output_single }
+function_output_stream = { CURLY_OPEN ~ function_output_type ~ ( COMMA ~ function_output_type )* ~ CURLY_CLOSE }
+function_output_single = { function_output_type ~ ( COMMA ~ function_output_type )* }
+function_output_type = { value_type_optional | value_type_list | value_type }
 
 return_statement = { RETURN ~ ( return_statement_stream | return_statement_single ) ~ SEMICOLON }
-return_statement_stream = { CURLY_OPEN ~ VAR ~ ( COMMA ~ VAR )* ~ CURLY_CLOSE }
-return_statement_single = { ( VAR | reduce ) ~ ( COMMA ~ ( VAR | reduce ) )* }
+return_statement_stream = { CURLY_OPEN ~ vars ~ CURLY_CLOSE }
+return_statement_single = { ( var | reduce ) ~ ( COMMA ~ ( var | reduce ) )* }
 
-// STRUCT CONSTRUCTS ===========================================================
+// STRUCT DEFINITION ===========================================================
 
-definition_struct = { STRUCT ~ label ~ COLON ~ definition_struct_fields ~ SEMICOLON }
+definition_struct = { STRUCT ~ identifier ~ COLON ~ definition_struct_fields ~ SEMICOLON }
 definition_struct_fields = { definition_struct_field ~ ( COMMA ~ definition_struct_field )* }
-definition_struct_field = { struct_key ~ VALUE ~ ( value_type_primitive | label ) }
+definition_struct_field = { identifier ~ VALUE ~ struct_field_value_type }
+
+struct_field_value_type = { value_type_optional | value_type }
 
 struct_destructor = { CURLY_OPEN ~ struct_key ~ COLON ~ struct_destructor_value }
-struct_destructor_value = { VAR | struct_destructor }
+struct_destructor_value = { var | struct_destructor }
+
+// REDEFINE QUERY ==============================================================
 
-struct_key = { label }
+query_redefine = { REDEFINE ~ ( redefinable ~ SEMICOLON )+ }
+redefinable = { kind? ~ identifier ~ type_capability }
+
+// UNDEFINE QUERY ==============================================================
+
+query_undefine = { UNDEFINE ~ undefinables }
+
+undefinables = { ( undefinable ~ SEMICOLON )+ }
+undefinable = { undefine_from | undefine_function | undefine_struct | label }
+
+undefine_from = { undefine_annotation_from_capability
+                | undefine_annotation_from_type
+                | undefine_capability
+                | undefine_override
+                }
+undefine_annotation_from_capability = { annotation_category ~ FROM ~ label ~ type_capability_base }
+undefine_annotation_from_type = { annotation_category ~ FROM ~ label }
+undefine_capability = { type_capability_base ~ FROM ~ label }
+undefine_override = { AS ~ label ~ FROM ~ label ~ type_capability_base }
+
+undefine_function = { FUN ~ identifier }
+undefine_struct = { STRUCT ~ identifier }
 
 // TYPE, LABEL AND IDENTIFIER CONSTRUCTS =======================================
 
-type_ref_any = { type_ref_scoped | type_ref | VAR }
-type_ref_scoped = { label_scoped | VAR }
-type_ref = { label | VAR }
-type_ref_list = { list_label | LIST_VAR }
+label = ${ kind | ROLE | identifier } // type label
+label_scoped = ${ label ~ COLON ~ label ~ WB } // fully qualified role type label
 
-label_any = { label_scoped | label }
-label_scoped = { LABEL_SCOPED }
-label = { LABEL | type_native | unreserved }
-list_label = { LABEL ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
+identifier = @{ !reserved ~ IDENTIFIER_LABEL_H ~ IDENTIFIER_LABEL_T* ~ WB } // user defined type, function, struct, or struct field
 
-// LITERAL INPUT VALUES ========================================================
+type_ref_any = { type_ref_scoped | type_ref }
+type_ref_scoped = { label_scoped | var }
+type_ref = { label | var }
+type_ref_list = { label_list | var_list }
 
-type_native = { ENTITY | ATTRIBUTE | RELATION | ROLE }
+label_arg = { label_list | label }
+label_list = { label ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
 
-value_type_primitive = { LONG | DOUBLE | STRING | BOOLEAN | DATETIME }
-list_value_type_primitive = { value_type_primitive ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
+struct_key = { identifier }
 
-value_primitive = { QUOTED_STRING | DATETIME_VALUE | DATE_VALUE | BOOLEAN_
-                  | signed_double | signed_long
-                  }
+iid_value = @{ "0x" ~ ASCII_HEX_DIGIT+ ~ WB }
+
+// VARIABLES ===================================================================
 
-signed_double = { sign? ~ DOUBLE_VALUE }
-signed_long = { sign? ~ LONG_VALUE }
-sign = { ADD | SUBTRACT }
+vars = { var ~ ( COMMA ~ var )* }
 
-// UNRESERVED KEYWORDS =========================================================
-// Most of TypeQL syntax should not be reserved from being used as identifiers
+var = ${ VAR_ANONYMOUS | var_named }
+VAR_ANONYMOUS = @{ "$_" ~ WB }
+var_named = ${ "$" ~ identifier_var }
+identifier_var = @{ IDENTIFIER_VAR_H ~ IDENTIFIER_VAR_T* ~ WB }
+
+var_list = ${ var ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
+
+// ========================================================
+
+value_type = { value_type_primitive | identifier }
+value_type_optional = { value_type ~ QUESTION }
+value_type_list = { value_type ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
+
+value_type_primitive = { BOOLEAN | LONG | DOUBLE | DECIMAL
+                       | DATETIME_TZ | DATETIME | DATE | DURATION
+                       | STRING
+                       }
+value_literal = { quoted_string_literal | datetime_tz_literal | datetime_literal | date_literal
+                | boolean_literal | signed_decimal | signed_integer
+                }
+
+signed_decimal = { sign? ~ decimal_literal }
+signed_integer = { sign? ~ integer_literal }
+sign = { PLUS | MINUS }
+
+// ANNOTATIONS =================================================================
+
+annotations = { annotation+ }
+annotation = { ANNOTATION_ABSTRACT
+             | ANNOTATION_CASCADE
+             | ANNOTATION_DISTINCT
+             | ANNOTATION_INDEPENDENT
+             | ANNOTATION_KEY
+             | ANNOTATION_UNIQUE
+             | annotation_card
+             | annotation_range
+             | annotation_regex
+             | annotation_subkey
+             | annotation_values
+             }
 
-unreserved = { VALUE | FILTER | SORT | LIMIT | OFFSET | GROUP | LIKE | CONTAINS }
+annotation_card = { ANNOTATION_CARD ~ PAREN_OPEN ~ integer_literal ~ DOUBLE_DOT ~ integer_literal? ~ PAREN_CLOSE }
+annotation_regex = { ANNOTATION_REGEX ~ PAREN_OPEN ~ quoted_string_literal ~ PAREN_CLOSE }
+annotation_subkey = { ANNOTATION_SUBKEY ~ PAREN_OPEN ~ identifier ~ PAREN_CLOSE }
+annotation_range = { ANNOTATION_RANGE ~ PAREN_OPEN ~ range ~ PAREN_CLOSE }
+annotation_values = { ANNOTATION_VALUES ~ PAREN_OPEN ~ value_literal ~ ( COMMA ~ value_literal )* ~ PAREN_CLOSE }
+
+range = { range_full | range_from | range_to }
+range_from = { range_bound ~ DOUBLE_DOT }
+range_to = { DOUBLE_DOT ~ range_bound }
+range_full = { range_bound ~ DOUBLE_DOT ~ range_bound }
+range_bound = _{ value_literal }
 
 // TYPEQL SYNTAX KEYWORDS ======================================================
 
+reserved = { WITH | MATCH | FETCH | UPDATE | DEFINE | UNDEFINE | REDEFINE | INSERT | PUT | DELETE
+           | ENTITY | RELATION | ATTRIBUTE | ROLE
+           | ASC | DESC
+           | STRUCT | FUN | RETURN
+           | ALIAS | SUB | OWNS | AS | PLAYS | RELATES
+           | IID | ISA | LINKS | HAS
+           | IS | OR | NOT | TRY | IN
+           | OF
+           | TRUE | FALSE
+           }
+
+unreserved = { ABS | CEIL | FLOOR | ROUND | LENGTH
+             | CHECK | FIRST
+             | COUNT | MAX | MIN | MEAN | MEDIAN | STD | SUM
+             | VALUE | FILTER | SORT | LIMIT | OFFSET | GROUP | LIKE | CONTAINS
+             | BOOLEAN | LONG | DOUBLE | DECIMAL | DATETIME_TZ | DATETIME | DATE | DURATION | STRING
+             | FROM
+             }
+
 // QUERY COMMAND KEYWORDS
 
+WITH = @{ "with" ~ WB }
 MATCH = @{ "match" ~ WB }
 FETCH = @{ "fetch" ~ WB }
 DEFINE = @{ "define" ~ WB }
 UNDEFINE = @{ "undefine" ~ WB }
+REDEFINE = @{ "redefine" ~ WB }
 INSERT = @{ "insert" ~ WB }
+PUT = @{ "put" ~ WB }
+UPDATE = @{ "update" ~ WB }
 DELETE = @{ "delete" ~ WB }
-CHECK = @{ "check" }
+
+CHECK = @{ "check" ~ WB }
 FIRST = @{ "first" ~ WB }
 
-// NATIVE TYPE KEYWORDS
+// THING KIND KEYWORDS
 
+kind = { ENTITY | ATTRIBUTE | RELATION }
 ENTITY = @{ "entity" ~ WB }
 ATTRIBUTE = @{ "attribute" ~ WB }
 RELATION = @{ "relation" ~ WB }
 ROLE = @{ "role" ~ WB }
 
-// DELETE AND GET QUERY MODIFIER KEYWORDS
+// QUERY MODIFIER KEYWORDS
 
 FILTER = @{ "filter" ~ WB }
 LIMIT = @{ "limit" ~ WB }
 OFFSET = @{ "offset" ~ WB }
 SORT = @{ "sort" ~ WB }
-ORDER_ = @{ ASC | DESC }
+
+ORDER = ${ ASC | DESC }
 ASC = @{ "asc" ~ WB }
 DESC = @{ "desc" ~ WB }
 
@@ -326,17 +406,33 @@ RETURN = @{ "return" ~ WB }
 
 // TYPE VARIABLE CONSTRAINT KEYWORDS
 
-TYPE = @{ "type" ~ WB }
-SUB_ = ${ SUBX | SUB }
-SUB = @{ "sub" ~ WB }
-SUBX = @{ "sub!" ~ WB }
-OWNS = @{ "owns" ~ WB }
+ALIAS = @{ "alias" ~ WB }
 AS = @{ "as" ~ WB }
+FROM = @{ "from" ~ WB }
+LABEL = @{ "label" ~ WB }
+OF = @{ "of" ~ WB }
+OWNS = @{ "owns" ~ WB }
 PLAYS = @{ "plays" ~ WB }
 RELATES = @{ "relates" ~ WB }
+SUB_ = ${ SUBX | SUB }
+SUB = @{ "sub" ~ WB }
+SUBX = @{ "sub!" ~ WB }
 
 // TYPE ANNOTATIONS
 
+annotation_category = ${ ANNOTATION_ABSTRACT
+                       | ANNOTATION_CARD
+                       | ANNOTATION_CASCADE
+                       | ANNOTATION_DISTINCT
+                       | ANNOTATION_INDEPENDENT
+                       | ANNOTATION_KEY
+                       | ANNOTATION_RANGE
+                       | ANNOTATION_REGEX
+                       | ANNOTATION_SUBKEY
+                       | ANNOTATION_UNIQUE
+                       | ANNOTATION_VALUES
+                       }
+
 // unparametrized
 ANNOTATION_ABSTRACT = @{ "@abstract" ~ WB }
 ANNOTATION_CASCADE = @{ "@cascade" ~ WB }
@@ -346,14 +442,11 @@ ANNOTATION_INDEPENDENT = @{ "@independent" ~ WB }
 ANNOTATION_UNIQUE = @{ "@unique" ~ WB }
 
 // parametrized
-ANNOTATION_CARD = @{ "@card" }
-ANNOTATION_RANGE = @{ "@range" }
-ANNOTATION_REGEX = @{ "@regex" }
-ANNOTATION_SUBKEY = @{ "@subkey" }
-ANNOTATION_VALUES = @{ "@values" }
-
-ANNOTATION_CARD_LOWER = @{ ASCII_DIGIT+ }
-ANNOTATION_CARD_UPPER = @{ ASCII_DIGIT+ | "*" }
+ANNOTATION_CARD = @{ "@card" ~ WB }
+ANNOTATION_RANGE = @{ "@range" ~ WB }
+ANNOTATION_REGEX = @{ "@regex" ~ WB }
+ANNOTATION_SUBKEY = @{ "@subkey" ~ WB }
+ANNOTATION_VALUES = @{ "@values" ~ WB }
 
 // THING VARIABLE CONSTRAINT KEYWORDS
 
@@ -386,22 +479,22 @@ CONTAINS = @{ "contains" ~ WB }
 // ASSIGNMENT AND EXPRESSION KEYWORDS
 
 ASSIGN = @{ "=" }
-IN = @{ "in" }
+IN = @{ "in" ~ WB }
 
-ADD = @{ "+" }
-SUBTRACT = @{ "-" }
+PLUS = @{ "+" }
+MINUS = @{ "-" }
+TIMES = @{ "*" }
 DIVIDE = @{ "/" }
-MULTIPLY = @{ "*" }
 POWER = @{ "^" }
 MODULO = @{ "%" }
 
 // Incomplete list of function names usable in expressions. The 'expression_function_name' rule references all function names.
-EXPR_FUNC_NAME = _{ ABS | CEIL | FLOOR | ROUND | LENGTH }
-ABS = @{ "abs" }
-CEIL = @{ "ceil" }
-FLOOR = @{ "floor" }
-ROUND = @{ "round" }
+builtin_func_name = ${ ABS | CEIL | FLOOR | ROUND | LENGTH | MAX | MIN }
+ABS = @{ "abs" ~ WB }
+CEIL = @{ "ceil" ~ WB }
+FLOOR = @{ "floor" ~ WB }
 LENGTH = @{ "length" ~ WB }
+ROUND = @{ "round" ~ WB }
 
 // GROUP AND AGGREGATE QUERY KEYWORDS (also used by COMPUTE QUERY)
 
@@ -416,32 +509,36 @@ SUM = @{ "sum" ~ WB }
 
 // VALUE TYPE KEYWORDS
 
+BOOLEAN = @{ "boolean" ~ WB }
 LONG = @{ "long" ~ WB }
 DOUBLE = @{ "double" ~ WB }
-STRING = @{ "string" ~ WB }
-BOOLEAN = @{ "boolean" ~ WB }
+DECIMAL = @{ "decimal" ~ WB }
+
+DATE = @{ "date" ~ WB }
 DATETIME = @{ "datetime" ~ WB }
+DATETIME_TZ = @{ "datetime-tz" ~ WB }
+DURATION = @{ "duration" ~ WB }
+
+STRING = @{ "string" ~ WB }
 STRUCT = @{ "struct" ~ WB }
 
 // LITERAL VALUE KEYWORDS
-BOOLEAN_ = ${ TRUE | FALSE } // order of lexer declaration matters
+boolean_literal = ${ TRUE | FALSE } // order of lexer declaration matters
 TRUE = @{ "true" ~ WB }
 FALSE = @{ "false" ~ WB }
-QUOTED_STRING = @{ "\"" ~ ( !"\"" ~ !"\\" ~ ANY | ESCAPE_SEQ )* ~ "\""
-                 | "'" ~ ( !"'" ~ !"\\" ~ ANY | ESCAPE_SEQ )* ~ "'" }
-LONG_VALUE = @{ ASCII_DIGIT+ ~ WB }
-DOUBLE_VALUE = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ ~ WB }
-DATE_VALUE = ${ DATE_FRAGMENT ~ WB }
-DATETIME_VALUE = ${ DATE_FRAGMENT ~ "T" ~ TIME ~ WB }
-
-// TYPEQL INPUT PATTERNS
-VAR = ${ VAR_ANONYMOUS | VAR_NAMED }
-VAR_ANONYMOUS = @{ "$_" ~ WB }
-VAR_NAMED = @{ "$" ~ IDENTIFIER_VAR_H ~ IDENTIFIER_VAR_T* ~ WB }
-LIST_VAR = @{ VAR ~ SQ_BRACKET_OPEN ~ SQ_BRACKET_CLOSE }
-IID_VALUE = @{ "0x" ~ ASCII_HEX_DIGIT+ ~ WB }
-LABEL = @{ IDENTIFIER_LABEL_H ~ IDENTIFIER_LABEL_T* ~ WB }
-LABEL_SCOPED = ${ LABEL ~ ":" ~ LABEL ~ WB }
+
+integer_literal = @{ ASCII_DIGIT+ }
+decimal_literal = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ ~ ( ^"e" ~ sign? ~ integer_literal )? }
+numeric_literal = @{ decimal_literal | integer_literal }
+
+date_literal = ${ date_fragment ~ WB }
+datetime_literal = ${ date_fragment ~ "T" ~ time ~ WB }
+datetime_tz_literal = ${ date_fragment ~ "T" ~ time ~ ( " " ~ iana_timezone | iso8601_timezone_offset ) ~ WB }
+
+duration_literal = ${ "P" ~ ( duration_weeks | duration_date ~ ( "T" ~ duration_time )? ) ~ WB}
+
+quoted_string_literal = @{ "\"" ~ ( !"\"" ~ !"\\" ~ ANY | escape_seq )* ~ "\""
+                         | "'" ~ ( !"'" ~ !"\\" ~ ANY | escape_seq )* ~ "'" }
 
 // PUNCTUATION =================================================================
 
@@ -454,6 +551,7 @@ CURLY_CLOSE = _{ "}" }
 COMMA = _{ "," }
 SEMICOLON = _{ ";" }
 COLON = _{ ":" }
+DOUBLE_DOT = _{ ".." }
 QUESTION = _{ "?" }
 ARROW = _{ "->" }
 
@@ -485,18 +583,41 @@ IDENTIFIER_LABEL_T = @{ IDENTIFIER_LABEL_H | ASCII_DIGIT | IDENTIFIER_CONNECTOR
 IDENTIFIER_VAR_H = @{ IDENTIFIER_CHAR | ASCII_DIGIT }
 IDENTIFIER_VAR_T = @{ IDENTIFIER_VAR_H | IDENTIFIER_CONNECTOR }
 
-DATE_FRAGMENT = ${ YEAR ~ "-" ~ MONTH ~ "-" ~ DAY }
-MONTH = @{ ('0'..'1') ~ ASCII_DIGIT }
-DAY = @{ ('0'..'3') ~ ASCII_DIGIT }
-YEAR = @{ ASCII_DIGIT{4} | ( "+" | "-" ) ~ ASCII_DIGIT+ }
-TIME = ${ HOUR ~ COLON ~ MINUTE ~ ( COLON ~ SECOND ~ ( "." ~ SECOND_FRACTION )? )? }
-HOUR = @{ ( '0'..'2' ) ~ ASCII_DIGIT }
-MINUTE = @{ ( '0'..'6' ) ~ ASCII_DIGIT }
-SECOND = @{ ( '0'..'6' ) ~ ASCII_DIGIT }
-SECOND_FRACTION = @{ ASCII_DIGIT{1,3} } // between 1 and 3 digits
-ESCAPE_SEQ = @{ "\\" ~ ANY }
-
-WB = _{ &( PUNCTUATION | WHITESPACE | COMMENT | EOI ) } // Word boundary
+date_fragment = ${ year ~ "-" ~ month ~ "-" ~ day }
+month = @{ "0" ~ ( '1'..'9' ) | "10" | "11" | "12" }
+day = @{ ( '0'..'3' ) ~ ASCII_DIGIT }
+year = @{ ASCII_DIGIT{4} | ( "+" | "-" ) ~ ASCII_DIGIT+ }
+time = ${ hour ~ COLON ~ minute ~ ( COLON ~ second ~ ( "." ~ second_fraction )? )? }
+hour = @{ ( '0'..'2' ) ~ ASCII_DIGIT }
+minute = @{ ( '0'..'5' ) ~ ASCII_DIGIT }
+second = @{ ( '0'..'6' ) ~ ASCII_DIGIT }
+second_fraction = @{ ASCII_DIGIT{1,9} } // between 1 and 9 digits
+
+iso8601_timezone_offset = @{ "Z" | sign ~ hour ~ ( COLON? ~ minute )? } // "Z", "±HH", "±HHMM", "±HH:MM"
+
+iana_timezone = @{ iana_timezone_single ~ ( "/" ~ iana_timezone_single ){0,2} ~ WB }
+iana_timezone_single = @{ 'A'..'Z' ~ ( ASCII_ALPHANUMERIC | "_" | "-" | "+" )+ }
+
+duration_date = ${ duration_years ~ duration_months? ~ duration_days?
+                 | duration_months ~ duration_days?
+                 | duration_days
+                 }
+duration_years = ${ integer_literal ~ "Y" }
+duration_months = ${ integer_literal ~ "M" }
+duration_weeks = ${ integer_literal ~ "W" }
+duration_days = ${ integer_literal ~ "D" }
+
+duration_time = ${ duration_hours ~ duration_minutes? ~ duration_seconds?
+                 | duration_minutes ~ duration_seconds?
+                 | duration_seconds
+                 }
+duration_hours = ${ integer_literal ~ "H" }
+duration_minutes = ${ integer_literal ~ "M" }
+duration_seconds = ${ numeric_literal ~ "S" }
+
+escape_seq = @{ "\\" ~ ANY }
+
+WB = _{ &( !IDENTIFIER_CONNECTOR ~ PUNCTUATION | WHITESPACE | COMMENT | EOI ) } // Word boundary
 
 COMMENT = _{ "#" ~ ( !NEWLINE ~ ANY )* ~ ( NEWLINE | EOI ) }
 WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
diff --git a/rust/parser/undefine/mod.rs b/rust/parser/undefine/mod.rs
new file mode 100644
index 000000000..66ea7554e
--- /dev/null
+++ b/rust/parser/undefine/mod.rs
@@ -0,0 +1,142 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::{IntoChildNodes, Node, Rule, RuleMatcher};
+use crate::{
+    common::{error::TypeQLError, token, Spanned},
+    parser::{define::type_::visit_type_capability_base, visit_identifier, visit_label},
+    query::schema::Undefine,
+    schema::undefinable::{
+        AnnotationCapability, AnnotationType, CapabilityType, Function, Override, Struct, Undefinable,
+    },
+};
+
+pub(super) fn visit_query_undefine(node: Node<'_>) -> Undefine {
+    debug_assert_eq!(node.as_rule(), Rule::query_undefine);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::UNDEFINE);
+    let undefinables = visit_undefinables(children.consume_expected(Rule::undefinables));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Undefine::new(span, undefinables)
+}
+
+fn visit_undefinables(node: Node<'_>) -> Vec<Undefinable> {
+    debug_assert_eq!(node.as_rule(), Rule::undefinables);
+    node.into_children().map(visit_undefinable).collect()
+}
+
+fn visit_undefinable(node: Node<'_>) -> Undefinable {
+    debug_assert_eq!(node.as_rule(), Rule::undefinable);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::undefine_from => visit_undefine_from(child),
+        Rule::undefine_struct => Undefinable::Struct(visit_undefine_struct(child)),
+        Rule::undefine_function => Undefinable::Function(visit_undefine_function(child)),
+        Rule::label => Undefinable::Type(visit_label(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_undefine_from(node: Node<'_>) -> Undefinable {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_from);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::undefine_annotation_from_capability => {
+            Undefinable::AnnotationCapability(visit_undefine_annotation_from_capability(child))
+        }
+        Rule::undefine_annotation_from_type => Undefinable::AnnotationType(visit_undefine_annotation_from_type(child)),
+        Rule::undefine_capability => Undefinable::CapabilityType(visit_undefine_capability(child)),
+        Rule::undefine_override => Undefinable::Override(visit_undefine_override(child)),
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_undefine_annotation_from_capability(node: Node<'_>) -> AnnotationCapability {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_annotation_from_capability);
+    let span = node.span();
+    let mut children = node.into_children();
+    let annotation_category = visit_annotation_category(children.consume_expected(Rule::annotation_category));
+    children.skip_expected(Rule::FROM);
+    let type_ = visit_label(children.consume_expected(Rule::label));
+    let capability = visit_type_capability_base(children.consume_expected(Rule::type_capability_base));
+    debug_assert_eq!(children.try_consume_any(), None);
+    AnnotationCapability::new(span, annotation_category, type_, capability)
+}
+
+fn visit_undefine_annotation_from_type(node: Node<'_>) -> AnnotationType {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_annotation_from_type);
+    let span = node.span();
+    let mut children = node.into_children();
+    let annotation_category = visit_annotation_category(children.consume_expected(Rule::annotation_category));
+    children.skip_expected(Rule::FROM);
+    let type_ = visit_label(children.consume_expected(Rule::label));
+    debug_assert_eq!(children.try_consume_any(), None);
+    AnnotationType::new(span, annotation_category, type_)
+}
+
+fn visit_annotation_category(node: Node<'_>) -> token::Annotation {
+    debug_assert_eq!(node.as_rule(), Rule::annotation_category);
+    let child = node.into_child();
+    match child.as_rule() {
+        Rule::ANNOTATION_ABSTRACT => token::Annotation::Abstract,
+        Rule::ANNOTATION_CARD => token::Annotation::Cardinality,
+        Rule::ANNOTATION_CASCADE => token::Annotation::Cascade,
+        Rule::ANNOTATION_DISTINCT => token::Annotation::Distinct,
+        Rule::ANNOTATION_INDEPENDENT => token::Annotation::Independent,
+        Rule::ANNOTATION_KEY => token::Annotation::Key,
+        Rule::ANNOTATION_RANGE => token::Annotation::Range,
+        Rule::ANNOTATION_REGEX => token::Annotation::Regex,
+        Rule::ANNOTATION_SUBKEY => token::Annotation::Subkey,
+        Rule::ANNOTATION_UNIQUE => token::Annotation::Unique,
+        Rule::ANNOTATION_VALUES => token::Annotation::Values,
+        _ => unreachable!("{}", TypeQLError::IllegalGrammar { input: child.to_string() }),
+    }
+}
+
+fn visit_undefine_capability(node: Node<'_>) -> CapabilityType {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_capability);
+    let span = node.span();
+    let mut children = node.into_children();
+    let capability = visit_type_capability_base(children.consume_expected(Rule::type_capability_base));
+    children.skip_expected(Rule::FROM);
+    let type_ = visit_label(children.consume_expected(Rule::label));
+    debug_assert_eq!(children.try_consume_any(), None);
+    CapabilityType::new(span, capability, type_)
+}
+
+fn visit_undefine_override(node: Node<'_>) -> Override {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_override);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::AS);
+    let overridden = visit_label(children.consume_expected(Rule::label));
+    children.skip_expected(Rule::FROM);
+    let type_ = visit_label(children.consume_expected(Rule::label));
+    let capability = visit_type_capability_base(children.consume_expected(Rule::type_capability_base));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Override::new(span, overridden, type_, capability)
+}
+
+fn visit_undefine_struct(node: Node<'_>) -> Struct {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_struct);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::STRUCT);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Struct::new(span, ident)
+}
+
+fn visit_undefine_function(node: Node<'_>) -> Function {
+    debug_assert_eq!(node.as_rule(), Rule::undefine_function);
+    let span = node.span();
+    let mut children = node.into_children();
+    children.skip_expected(Rule::FUN);
+    let ident = visit_identifier(children.consume_expected(Rule::identifier));
+    debug_assert_eq!(children.try_consume_any(), None);
+    Function::new(span, ident)
+}
diff --git a/rust/pattern/label.rs b/rust/pattern/label.rs
deleted file mode 100644
index 6ca67e8bd..000000000
--- a/rust/pattern/label.rs
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::fmt;
-
-use crate::common::{error::TypeQLError, identifier::is_valid_label_identifier, Result, Span, Spanned};
-
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct Label {
-    pub scope: Option<String>,
-    pub name: String,
-    span: Option<Span>,
-}
-
-impl Label {
-    pub(crate) fn new_unscoped(name: impl Into<String>, span: Option<Span>) -> Self {
-        Self { scope: None, name: name.into(), span }
-    }
-
-    pub fn build(name: impl Into<String>) -> Self {
-        Self::new_unscoped(name, None)
-    }
-}
-
-impl Spanned for Label {
-    fn span(&self) -> Option<Span> {
-        self.span
-    }
-}
-
-fn validate_label(label: &str) -> Result {
-    if !is_valid_label_identifier(label) {
-        Err(TypeQLError::InvalidTypeLabel { label: label.to_owned() })?
-    } else {
-        Ok(())
-    }
-}
-
-impl fmt::Display for Label {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if let Some(scope) = &self.scope {
-            write!(f, "{scope}:")?;
-        }
-        write!(f, "{}", self.name)
-    }
-}
diff --git a/rust/pattern/mod.rs b/rust/pattern/mod.rs
index be909b5c0..f72b23838 100644
--- a/rust/pattern/mod.rs
+++ b/rust/pattern/mod.rs
@@ -4,31 +4,217 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-use std::fmt;
+use std::fmt::{self, Write};
 
-pub use self::{label::Label, statement::TypeDeclaration};
-use crate::{enum_getter, enum_wrapper};
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    statement::{thing::Thing, Statement},
+};
 
-pub mod label;
-pub(crate) mod statement;
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Conjunction {
+    span: Option<Span>,
+    patterns: Vec<Pattern>,
+}
+
+impl Conjunction {
+    pub(crate) fn new(span: Option<Span>, patterns: Vec<Pattern>) -> Self {
+        Self { span, patterns }
+    }
+}
+
+impl Pretty for Conjunction {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        pretty_fmt_patterns(&self.patterns, indent_level, f)
+    }
+}
+
+impl fmt::Display for Conjunction {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_char('{')?;
+        for pattern in &self.patterns {
+            writeln!(f, " {}; ", pattern)?;
+        }
+        f.write_char('}')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Negation {
+    span: Option<Span>,
+    patterns: Vec<Pattern>,
+}
+
+impl Negation {
+    pub(crate) fn new(span: Option<Span>, patterns: Vec<Pattern>) -> Self {
+        Self { span, patterns }
+    }
+}
+
+impl Pretty for Negation {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ", token::Keyword::Not)?;
+        pretty_fmt_patterns(&self.patterns, indent_level + 1, f)?;
+        Ok(())
+    }
+}
+
+impl fmt::Display for Negation {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {{", token::Keyword::Not)?;
+        for pattern in &self.patterns {
+            writeln!(f, " {}; ", pattern)?;
+        }
+        f.write_char('}')?;
+        Ok(())
+    }
+}
 
 #[derive(Debug, Clone, Eq, PartialEq)]
-pub enum Definable {
-    TypeDeclaration(TypeDeclaration),
+pub struct Try {
+    span: Option<Span>,
+    patterns: Vec<Pattern>,
 }
 
-enum_getter! { Definable
-    into_type_statement(TypeDeclaration) => TypeDeclaration,
+impl Try {
+    pub(crate) fn new(span: Option<Span>, patterns: Vec<Pattern>) -> Self {
+        Self { span, patterns }
+    }
 }
 
-enum_wrapper! { Definable
-    TypeDeclaration => TypeDeclaration,
+impl Pretty for Try {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ", token::Keyword::Not)?;
+        pretty_fmt_patterns(&self.patterns, indent_level + 1, f)?;
+        Ok(())
+    }
 }
 
-impl fmt::Display for Definable {
+impl fmt::Display for Try {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {{", token::Keyword::Try)?;
+        for pattern in &self.patterns {
+            writeln!(f, " {}; ", pattern)?;
+        }
+        f.write_char('}')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Disjunction {
+    span: Option<Span>,
+    branches: Vec<Vec<Pattern>>,
+}
+
+impl Disjunction {
+    pub(crate) fn new(span: Option<Span>, branches: Vec<Vec<Pattern>>) -> Self {
+        Self { span, branches }
+    }
+}
+
+impl Pretty for Disjunction {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some((first, rest)) = self.branches.split_first() {
+            f.write_str("{\n")?;
+            for pattern in first {
+                indent(indent_level + 1, f)?;
+                Pretty::fmt(pattern, indent_level + 1, f)?;
+                writeln!(f, ";")?;
+                indent(indent_level, f)?;
+            }
+            f.write_char('}')?;
+
+            for branch in rest {
+                writeln!(f, " {} {{", token::Keyword::Or)?;
+                for pattern in branch {
+                    indent(indent_level + 1, f)?;
+                    Pretty::fmt(pattern, indent_level + 1, f)?;
+                    writeln!(f, ";")?;
+                    indent(indent_level, f)?;
+                }
+                f.write_char('}')?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Disjunction {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some((first, rest)) = self.branches.split_first() {
+            f.write_char('{')?;
+            for pattern in first {
+                write!(f, " {};", pattern)?;
+            }
+            f.write_str(" }")?;
+
+            for branch in rest {
+                write!(f, " {} {{", token::Keyword::Or)?;
+                for pattern in branch {
+                    write!(f, " {};", pattern)?;
+                }
+                f.write_str(" }")?;
+            }
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Pattern {
+    Conjunction(Conjunction),
+    Disjunction(Disjunction),
+    Negation(Negation),
+    Try(Try),
+    Statement(Statement),
+}
+
+impl From<Thing> for Pattern {
+    fn from(val: Thing) -> Self {
+        Pattern::Statement(Statement::Thing(val))
+    }
+}
+
+impl Pretty for Pattern {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Conjunction(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Disjunction(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Negation(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Try(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Statement(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Pattern {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Self::TypeDeclaration(declaration) => fmt::Display::fmt(declaration, f),
+            Self::Conjunction(inner) => fmt::Display::fmt(inner, f),
+            Self::Disjunction(inner) => fmt::Display::fmt(inner, f),
+            Self::Negation(inner) => fmt::Display::fmt(inner, f),
+            Self::Try(inner) => fmt::Display::fmt(inner, f),
+            Self::Statement(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+fn pretty_fmt_patterns(patterns: &[Pattern], indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    match patterns {
+        [Pattern::Statement(pattern)] => {
+            write!(f, "{{ {}; }}", pattern)?;
+        }
+        patterns => {
+            f.write_str("{\n")?;
+            for pattern in patterns {
+                indent(indent_level, f)?;
+                writeln!(f, "{};", pattern)?;
+            }
+            f.write_char('}')?;
         }
     }
+    Ok(())
 }
diff --git a/rust/pattern/statement/type_.rs b/rust/pattern/statement/type_.rs
deleted file mode 100644
index 5b49389a4..000000000
--- a/rust/pattern/statement/type_.rs
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::fmt::{self, Write};
-
-use self::declaration::{OwnsDeclaration, PlaysDeclaration, RelatesDeclaration, SubDeclaration, ValueTypeDeclaration};
-use crate::{
-    common::{Span, Spanned},
-    pattern::Label,
-    write_joined,
-};
-
-pub mod declaration {
-    use std::fmt::{self, Write};
-
-    use crate::{common::Span, pattern::Label, write_joined};
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum AnnotationSub {
-        Abstract(Option<Span>),    // FIXME
-        Cascade(Option<Span>),     // FIXME
-        Independent(Option<Span>), // FIXME
-    }
-
-    impl fmt::Display for AnnotationSub {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::Abstract(_) => f.write_str("@abstract"),
-                Self::Cascade(_) => f.write_str("@cascade"),
-                Self::Independent(_) => f.write_str("@independent"),
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct SubDeclaration {
-        pub supertype_label: Label,
-        pub annotations: Vec<AnnotationSub>,
-        span: Option<Span>,
-    }
-
-    impl SubDeclaration {
-        pub fn new(supertype_label: Label, annotations: Vec<AnnotationSub>, span: Option<Span>) -> Self {
-            Self { supertype_label, annotations, span }
-        }
-    }
-
-    impl fmt::Display for SubDeclaration {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            f.write_str("sub ")?;
-            fmt::Display::fmt(&self.supertype_label, f)?;
-            if !self.annotations.is_empty() {
-                f.write_char(' ')?;
-                write_joined!(f, ' ', &self.annotations)?;
-            }
-            Ok(())
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum AnnotationValueType {
-        Regex(String, Option<Span>),       // FIXME
-        Values(Vec<String>, Option<Span>), // FIXME
-    }
-
-    impl fmt::Display for AnnotationValueType {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::Regex(regex, _) => write!(f, "@regex({regex})"),
-                Self::Values(values, _) => {
-                    f.write_str("@values(")?;
-                    write_joined!(f, ", ", values)?;
-                    f.write_char(')')?;
-                    Ok(())
-                }
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct ValueTypeDeclaration {
-        pub value_type: String, // TODO enum with optional user type?
-        pub annotations: Vec<AnnotationValueType>,
-        pub span: Option<Span>,
-    }
-
-    impl ValueTypeDeclaration {
-        pub fn new(value_type: String, annotations: Vec<AnnotationValueType>, span: Option<Span>) -> Self {
-            Self { value_type, annotations, span }
-        }
-    }
-
-    impl fmt::Display for ValueTypeDeclaration {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            f.write_str("value ")?;
-            fmt::Display::fmt(&self.value_type, f)?;
-            if !self.annotations.is_empty() {
-                f.write_char(' ')?;
-                write_joined!(f, ' ', &self.annotations)?;
-            }
-            Ok(())
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum AnnotationOwns {
-        Cardinality(usize, Option<usize>, Option<Span>), // FIXME
-        Distinct(Option<Span>),                          // FIXME
-        Key(Option<Span>),                               // FIXME
-        Unique(Option<Span>),                            // FIXME
-    }
-
-    impl fmt::Display for AnnotationOwns {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::Cardinality(min, max, _) => {
-                    f.write_str("@card(")?;
-                    fmt::Display::fmt(min, f)?;
-                    f.write_str(", ")?;
-                    match max {
-                        Some(max) => fmt::Display::fmt(max, f)?,
-                        None => f.write_char('*')?,
-                    }
-                    f.write_char(')')?;
-                    Ok(())
-                }
-                Self::Distinct(_) => f.write_str("@distinct"),
-                Self::Key(_) => f.write_str("@key"),
-                Self::Unique(_) => f.write_str("@unique"),
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum Owned {
-        List(Label),
-        Attribute(Label, Option<Label>),
-    }
-
-    impl fmt::Display for Owned {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::List(label) => write!(f, "{label}[]"),
-                Self::Attribute(label, None) => write!(f, "{label}"),
-                Self::Attribute(label, Some(overridden)) => write!(f, "{label} as {overridden}"),
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct OwnsDeclaration {
-        pub owned: Owned,
-        pub annotations: Vec<AnnotationOwns>,
-        span: Option<Span>,
-    }
-
-    impl OwnsDeclaration {
-        pub fn new(owned: Owned, annotations: Vec<AnnotationOwns>, span: Option<Span>) -> Self {
-            Self { owned, annotations, span }
-        }
-    }
-
-    impl fmt::Display for OwnsDeclaration {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            f.write_str("owns ")?;
-            fmt::Display::fmt(&self.owned, f)?;
-            if !self.annotations.is_empty() {
-                f.write_char(' ')?;
-                write_joined!(f, ' ', &self.annotations)?;
-            }
-            Ok(())
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum AnnotationRelates {
-        Cardinality(usize, Option<usize>, Option<Span>), // FIXME
-        Distinct(Option<Span>),                          // FIXME
-        Cascade(Option<Span>),                           // FIXME
-    }
-
-    impl fmt::Display for AnnotationRelates {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::Cardinality(min, max, _) => {
-                    f.write_str("@card(")?;
-                    fmt::Display::fmt(min, f)?;
-                    f.write_str(", ")?;
-                    match max {
-                        Some(max) => fmt::Display::fmt(max, f)?,
-                        None => f.write_char('*')?,
-                    }
-                    f.write_char(')')?;
-                    Ok(())
-                }
-                Self::Distinct(_) => f.write_str("@distinct"),
-                Self::Cascade(_) => f.write_str("@cascade"),
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub enum Related {
-        List(Label),
-        Attribute(Label, Option<Label>),
-    }
-
-    impl fmt::Display for Related {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            match self {
-                Self::List(label) => write!(f, "{label}[]"),
-                Self::Attribute(label, None) => write!(f, "{label}"),
-                Self::Attribute(label, Some(overridden)) => write!(f, "{label} as {overridden}"),
-            }
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct RelatesDeclaration {
-        pub related: Related,
-        pub annotations: Vec<AnnotationRelates>,
-        span: Option<Span>,
-    }
-
-    impl RelatesDeclaration {
-        pub fn new(related: Related, annotations: Vec<AnnotationRelates>, span: Option<Span>) -> Self {
-            Self { related, annotations, span }
-        }
-    }
-
-    impl fmt::Display for RelatesDeclaration {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            f.write_str("relates ")?;
-            fmt::Display::fmt(&self.related, f)?;
-            if !self.annotations.is_empty() {
-                f.write_char(' ')?;
-                write_joined!(f, ' ', &self.annotations)?;
-            }
-            Ok(())
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct Played {
-        role: Label,
-        overridden: Option<Label>,
-    }
-
-    impl Played {
-        pub fn new(role: Label, overridden: Option<Label>) -> Self {
-            Self { role, overridden }
-        }
-    }
-
-    impl fmt::Display for Played {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            write!(f, "{}", self.role)?;
-            if let Some(overridden) = &self.overridden {
-                write!(f, " as {overridden}")?;
-            }
-            Ok(())
-        }
-    }
-
-    #[derive(Debug, Clone, Eq, PartialEq)]
-    pub struct PlaysDeclaration {
-        pub played: Played,
-        span: Option<Span>,
-    }
-
-    impl PlaysDeclaration {
-        pub fn new(played: Played, span: Option<Span>) -> Self {
-            Self { played, span }
-        }
-    }
-
-    impl fmt::Display for PlaysDeclaration {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            f.write_str("plays ")?;
-            fmt::Display::fmt(&self.played, f)?;
-            Ok(())
-        }
-    }
-}
-
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct TypeDeclaration {
-    span: Option<Span>,
-    pub label: Label,
-    pub sub: Option<SubDeclaration>,
-    pub value_type: Option<ValueTypeDeclaration>,
-    pub owns: Vec<OwnsDeclaration>,
-    pub relates: Vec<RelatesDeclaration>,
-    pub plays: Vec<PlaysDeclaration>,
-}
-
-impl TypeDeclaration {
-    pub(crate) fn new(label: Label, span: Option<Span>) -> Self {
-        Self { span, label, sub: None, value_type: None, owns: Vec::new(), relates: Vec::new(), plays: Vec::new() }
-    }
-
-    pub fn set_sub(self, sub: SubDeclaration) -> Self {
-        Self { sub: Some(sub), ..self }
-    }
-
-    pub fn set_value_type(self, value_type: ValueTypeDeclaration) -> Self {
-        Self { value_type: Some(value_type), ..self }
-    }
-
-    pub fn set_owns(self, owns: Vec<OwnsDeclaration>) -> Self {
-        Self { owns, ..self }
-    }
-
-    pub fn add_owns(mut self, owns: OwnsDeclaration) -> Self {
-        self.owns.push(owns);
-        self
-    }
-
-    pub fn set_relates(self, relates: Vec<RelatesDeclaration>) -> Self {
-        Self { relates, ..self }
-    }
-
-    pub fn add_relates(mut self, relates: RelatesDeclaration) -> Self {
-        self.relates.push(relates);
-        self
-    }
-
-    pub fn set_plays(self, plays: Vec<PlaysDeclaration>) -> Self {
-        Self { plays, ..self }
-    }
-
-    pub fn add_plays(mut self, plays: PlaysDeclaration) -> Self {
-        self.plays.push(plays);
-        self
-    }
-}
-
-impl fmt::Display for TypeDeclaration {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        fmt::Display::fmt(&self.label, f)?;
-        f.write_char(' ')?;
-        let joiner = if f.alternate() { ",\n    " } else { ", " };
-        write_joined!(f, joiner, &self.sub, &self.value_type, &self.relates, &self.plays, &self.owns)?;
-        Ok(())
-    }
-}
-
-impl Spanned for TypeDeclaration {
-    fn span(&self) -> Option<Span> {
-        self.span
-    }
-}
diff --git a/rust/pretty.rs b/rust/pretty.rs
new file mode 100644
index 000000000..396ac3332
--- /dev/null
+++ b/rust/pretty.rs
@@ -0,0 +1,29 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+const INDENT: &str = "    ";
+
+pub(crate) fn indent(indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    for _ in 0..indent_level {
+        f.write_str(INDENT)?;
+    }
+    Ok(())
+}
+
+pub trait Pretty: fmt::Display {
+    #[allow(unused_variables)]
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self)
+    }
+}
+
+impl<T: Pretty> Pretty for Box<T> {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        <T as Pretty>::fmt(self, indent_level, f)
+    }
+}
diff --git a/rust/query/match_clause.rs b/rust/query/match_clause.rs
deleted file mode 100644
index 462664db0..000000000
--- a/rust/query/match_clause.rs
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{fmt, iter};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Error,
-    },
-    pattern::{Conjunction, Pattern, VariablesRetrieved},
-    query::{
-        modifier::Modifiers, typeql_get, Projection, TypeQLDelete, TypeQLFetch, TypeQLGet, TypeQLInsert, Writable,
-    },
-    variable::{variable::VariableRef, Variable},
-    Result,
-};
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct MatchClause {
-    pub conjunction: Conjunction,
-}
-
-impl MatchClause {
-    pub fn new(conjunction: Conjunction) -> Self {
-        Self { conjunction }
-    }
-
-    pub fn get(self) -> TypeQLGet {
-        TypeQLGet {
-            match_clause: self,
-            filter: typeql_get::Filter { vars: Vec::default() },
-            modifiers: Modifiers::default(),
-        }
-    }
-
-    pub fn get_vars(self, vars: Vec<Variable>) -> TypeQLGet {
-        TypeQLGet { match_clause: self, filter: typeql_get::Filter { vars }, modifiers: Modifiers::default() }
-    }
-
-    pub fn get_fixed<const N: usize, T: Into<Variable>>(self, vars: [T; N]) -> TypeQLGet {
-        self.get_vars(vars.into_iter().map(|var| var.into()).collect())
-    }
-
-    pub fn fetch(self, projections: Vec<Projection>) -> TypeQLFetch {
-        TypeQLFetch { match_clause: self, projections, modifiers: Modifiers::default() }
-    }
-
-    pub fn fetch_fixed<const N: usize>(self, projections: [Projection; N]) -> TypeQLFetch {
-        self.fetch(projections.into())
-    }
-
-    pub fn insert(self, writable: impl Writable) -> TypeQLInsert {
-        TypeQLInsert { match_clause: Some(self), statements: writable.statements(), modifiers: Modifiers::default() }
-    }
-
-    pub fn delete(self, writable: impl Writable) -> TypeQLDelete {
-        TypeQLDelete { match_clause: self, statements: writable.statements(), modifiers: Modifiers::default() }
-    }
-
-    fn validate_nested_patterns_are_bounded(&self) -> Result {
-        let bounds = self.conjunction.retrieved_variables().collect();
-        collect_err(self.conjunction.patterns.iter().map(|p| p.validate_is_bounded_by(&bounds)))
-    }
-}
-
-impl VariablesRetrieved for MatchClause {
-    fn retrieved_variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        self.conjunction.retrieved_variables()
-    }
-}
-
-impl Validatable for MatchClause {
-    fn validate(&self) -> Result {
-        self.validate_nested_patterns_are_bounded()?;
-        validate_statements_have_named_variable(self.conjunction.patterns.iter())?;
-        collect_err(self.conjunction.patterns.iter().map(|p| p.validate()))
-    }
-}
-
-fn validate_statements_have_named_variable<'a>(patterns: impl Iterator<Item = &'a Pattern>) -> Result {
-    collect_err(patterns.map(|pattern| {
-        match pattern {
-            Pattern::Statement(statement) => {
-                statement.variables().any(|variable| variable.is_name()).then_some(()).ok_or_else(|| {
-                    Error::from(TypeQLError::MatchStatementHasNoNamedVariable { pattern: pattern.clone() })
-                })
-            }
-            Pattern::Conjunction(c) => validate_statements_have_named_variable(c.patterns.iter()),
-            Pattern::Disjunction(d) => validate_statements_have_named_variable(d.patterns.iter()),
-            Pattern::Negation(n) => validate_statements_have_named_variable(iter::once(n.pattern.as_ref())),
-        }
-    }))
-}
-
-impl fmt::Display for MatchClause {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", token::Clause::Match)?;
-
-        for pattern in &self.conjunction.patterns {
-            write!(f, "\n{pattern};")?;
-        }
-
-        Ok(())
-    }
-}
diff --git a/rust/query/mod.rs b/rust/query/mod.rs
index 4a179dbad..b1dec39f8 100644
--- a/rust/query/mod.rs
+++ b/rust/query/mod.rs
@@ -6,49 +6,38 @@
 
 use std::fmt;
 
-pub use self::typeql_define::TypeQLDefine;
-use crate::{common::{Span, Spanned}, enum_getter};
+use self::pipeline::stage::{Match, Stage};
+pub use self::{
+    pipeline::{stage, Pipeline},
+    schema::SchemaQuery,
+};
+use crate::util::enum_getter;
 
-mod typeql_define;
-
-#[derive(Debug, Eq, PartialEq)]
-pub enum SchemaQuery {
-    Define(TypeQLDefine),
-}
-
-enum_getter! { SchemaQuery
-    into_define(Define) => TypeQLDefine,
-}
-
-impl Spanned for SchemaQuery {
-    fn span(&self) -> Option<Span> {
-        match self {
-            SchemaQuery::Define(define) => define.span(),
-        }
-    }
-}
-
-impl fmt::Display for SchemaQuery {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Self::Define(define_query) => fmt::Display::fmt(define_query, f),
-        }
-    }
-}
+pub mod pipeline;
+pub mod schema;
 
 #[derive(Debug, Eq, PartialEq)]
 pub enum Query {
     Schema(SchemaQuery),
+    Pipeline(Pipeline),
 }
 
 enum_getter! { Query
     into_schema(Schema) => SchemaQuery,
+    into_pipeline(Pipeline) => Pipeline,
 }
 
 impl fmt::Display for Query {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
             Self::Schema(schema_query) => fmt::Display::fmt(schema_query, f),
+            Self::Pipeline(data_query) => fmt::Display::fmt(data_query, f),
         }
     }
 }
+
+impl From<Match> for Query {
+    fn from(value: Match) -> Self {
+        Self::Pipeline(Pipeline::new(None, Vec::new(), vec![Stage::Match(value)]))
+    }
+}
diff --git a/rust/query/modifier.rs b/rust/query/modifier.rs
deleted file mode 100644
index 0f6aa28c8..000000000
--- a/rust/query/modifier.rs
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token, Result,
-    },
-    write_joined,
-};
-use crate::variable::variable::VariableRef;
-
-#[derive(Clone, Debug, Eq, PartialEq, Default)]
-pub struct Modifiers {
-    pub sorting: Option<Sorting>,
-    pub limit: Option<Limit>,
-    pub offset: Option<Offset>,
-}
-
-impl Modifiers {
-    pub fn is_empty(&self) -> bool {
-        self.sorting.is_none() && self.limit.is_none() && self.offset.is_none()
-    }
-
-    pub fn sort(self, sorting: impl Into<Sorting>) -> Self {
-        Self { sorting: Some(sorting.into()), ..self }
-    }
-
-    pub fn limit(self, limit: usize) -> Self {
-        Self { limit: Some(Limit { limit }), ..self }
-    }
-
-    pub fn offset(self, offset: usize) -> Self {
-        Self { offset: Some(Offset { offset }), ..self }
-    }
-}
-
-impl fmt::Display for Modifiers {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if !self.is_empty() {
-            write_joined!(f, "; ", self.sorting, self.offset, self.limit)?;
-            f.write_str(";")
-        } else {
-            Ok(())
-        }
-    }
-}
-
-pub mod sorting {
-    use std::fmt;
-
-    use crate::{
-        common::token,
-    };
-    use crate::variable::Variable;
-
-    #[derive(Clone, Debug, Eq, PartialEq)]
-    pub struct SortVariable {
-        pub variable: Variable,
-        pub order: Option<token::Order>,
-    }
-
-    impl From<Variable> for SortVariable {
-        fn from(variable: Variable) -> Self {
-            SortVariable { variable, order: None }
-        }
-    }
-
-    impl From<(Variable, token::Order)> for SortVariable {
-        fn from(ordered_var: (Variable, token::Order)) -> Self {
-            let (variable, order) = ordered_var;
-            SortVariable { variable, order: Some(order) }
-        }
-    }
-
-    impl From<(Variable, Option<token::Order>)> for SortVariable {
-        fn from(ordered_var: (Variable, Option<token::Order>)) -> Self {
-            let (variable, order) = ordered_var;
-            SortVariable { variable, order }
-        }
-    }
-
-    impl fmt::Display for SortVariable {
-        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-            write!(f, "{}", self.variable)?;
-            if let Some(order) = self.order {
-                write!(f, " {order}")?;
-            }
-            Ok(())
-        }
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Sorting {
-    pub(crate) vars: Vec<sorting::SortVariable>,
-}
-
-impl Sorting {
-    pub fn new(vars: Vec<sorting::SortVariable>) -> Self {
-        Sorting { vars }
-    }
-
-    pub(crate) fn validate(&self, available_variables: &HashSet<VariableRef<'_>>) -> Result {
-        collect_err(self.vars.iter().map(|r| {
-            available_variables
-                .contains(&r.variable.as_ref())
-                .then_some(())
-                .ok_or_else(|| TypeQLError::SortVarNotBound { variable: r.variable.clone() }.into())
-        }))
-    }
-}
-
-impl<const N: usize, T: Into<sorting::SortVariable>> From<[T; N]> for Sorting {
-    fn from(ordered_vars: [T; N]) -> Self {
-        Self::new(ordered_vars.map(|ordered_var| ordered_var.into()).to_vec())
-    }
-}
-
-impl<'a, T: Into<sorting::SortVariable> + Clone> From<&'a [T]> for Sorting {
-    fn from(ordered_vars: &'a [T]) -> Self {
-        Self::new(ordered_vars.iter().map(|ordered_var| ordered_var.clone().into()).collect())
-    }
-}
-
-impl fmt::Display for Sorting {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} ", token::Modifier::Sort)?;
-        write_joined!(f, ", ", self.vars)
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Default)]
-pub struct Limit {
-    pub limit: usize,
-}
-
-impl fmt::Display for Limit {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} {}", token::Modifier::Limit, self.limit)
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Default)]
-pub struct Offset {
-    pub offset: usize,
-}
-
-impl fmt::Display for Offset {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} {}", token::Modifier::Offset, self.offset)
-    }
-}
diff --git a/rust/query/pipeline/mod.rs b/rust/query/pipeline/mod.rs
new file mode 100644
index 000000000..735f1f8c7
--- /dev/null
+++ b/rust/query/pipeline/mod.rs
@@ -0,0 +1,107 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use self::stage::Stage;
+use crate::{
+    common::{Span, Spanned},
+    pretty::{indent, Pretty},
+    schema::definable,
+};
+
+pub mod stage;
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Preamble {
+    span: Option<Span>,
+    function: definable::Function,
+}
+
+impl Preamble {
+    pub(crate) fn new(span: Option<Span>, function: definable::Function) -> Self {
+        Self { span, function }
+    }
+}
+
+impl Pretty for Preamble {
+    fn fmt(&self, _indent_level: usize, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+impl fmt::Display for Preamble {
+    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Pipeline {
+    span: Option<Span>,
+    pub preambles: Vec<Preamble>,
+    pub stages: Vec<Stage>,
+}
+
+impl Pipeline {
+    pub fn new(span: Option<Span>, preambles: Vec<Preamble>, stages: Vec<Stage>) -> Self {
+        Self { span, preambles, stages }
+    }
+
+    pub fn build() -> Self {
+        Self::new(None, Vec::new(), Vec::new())
+    }
+
+    pub fn then(mut self, stage: Stage) -> Self {
+        self.stages.push(stage);
+        self
+    }
+}
+
+impl Spanned for Pipeline {
+    fn span(&self) -> Option<Span> {
+        todo!()
+    }
+}
+
+impl Pretty for Pipeline {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for preamble in &self.preambles {
+            indent(indent_level, f)?;
+            Pretty::fmt(preamble, indent_level, f)?;
+            writeln!(f)?;
+        }
+        if let Some((last, rest)) = self.stages.split_last() {
+            for stage in rest {
+                indent(indent_level, f)?;
+                Pretty::fmt(stage, indent_level, f)?;
+                writeln!(f)?;
+            }
+            indent(indent_level, f)?;
+            Pretty::fmt(last, indent_level, f)?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Pipeline {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            for preamble in &self.preambles {
+                write!(f, "{} ", preamble)?;
+            }
+            if let Some((last, rest)) = self.stages.split_last() {
+                for stage in rest {
+                    write!(f, "{} ", stage)?;
+                }
+                write!(f, "{}", last)?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/delete.rs b/rust/query/pipeline/stage/delete.rs
new file mode 100644
index 000000000..853c9b104
--- /dev/null
+++ b/rust/query/pipeline/stage/delete.rs
@@ -0,0 +1,100 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    statement::thing::Relation,
+    variable::Variable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Delete {
+    span: Option<Span>,
+    deletables: Vec<Deletable>,
+}
+
+impl Delete {
+    pub fn new(span: Option<Span>, deletables: Vec<Deletable>) -> Self {
+        Self { span, deletables }
+    }
+}
+
+impl Pretty for Delete {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Delete)?;
+        for deletable in &self.deletables {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(deletable, indent_level, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Delete {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Delete)?;
+            for deletable in &self.deletables {
+                write!(f, " {deletable};")?;
+            }
+            Ok(())
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Deletable {
+    span: Option<Span>,
+    kind: DeletableKind,
+}
+
+impl Deletable {
+    pub fn new(span: Option<Span>, kind: DeletableKind) -> Self {
+        Self { span, kind }
+    }
+}
+
+impl Pretty for Deletable {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        Pretty::fmt(&self.kind, indent_level, f)
+    }
+}
+
+impl fmt::Display for Deletable {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(&self.kind, f)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum DeletableKind {
+    Has { attribute: Variable, owner: Variable },
+    Links { players: Relation, relation: Variable },
+    Concept { variable: Variable },
+}
+
+impl Pretty for DeletableKind {}
+
+impl fmt::Display for DeletableKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Has { attribute, owner } => {
+                write!(f, "{} {} {} {}", token::Keyword::Has, attribute, token::Keyword::Of, owner)
+            }
+            Self::Links { players, relation } => {
+                write!(f, "{} {} {} {}", token::Keyword::Links, players, token::Keyword::Of, relation)
+            }
+            Self::Concept { variable } => write!(f, "{}", variable),
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/fetch.rs b/rust/query/pipeline/stage/fetch.rs
new file mode 100644
index 000000000..187f98b75
--- /dev/null
+++ b/rust/query/pipeline/stage/fetch.rs
@@ -0,0 +1,222 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    query::Pipeline,
+    type_::Type,
+    util::write_joined,
+    variable::Variable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Fetch {
+    span: Option<Span>,
+    projections: Vec<Projection>,
+}
+
+impl Fetch {
+    pub fn new(span: Option<Span>, projections: Vec<Projection>) -> Self {
+        Self { span, projections }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Projection {
+    Variable(ProjectionKeyVar),
+    Attribute(ProjectionKeyVar, Vec<ProjectionAttribute>),
+    Subquery(ProjectionKeyLabel, Pipeline),
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct ProjectionKeyVar {
+    variable: Variable,
+    label: Option<ProjectionKeyLabel>,
+}
+
+impl ProjectionKeyVar {
+    pub fn new(variable: Variable, label: Option<ProjectionKeyLabel>) -> Self {
+        Self { variable, label }
+    }
+
+    pub fn label(self, label: impl Into<ProjectionKeyLabel>) -> Self {
+        ProjectionKeyVar { label: Some(label.into()), ..self }
+    }
+}
+
+impl<T: Into<ProjectionKeyVar>> From<T> for Projection {
+    fn from(key_var: T) -> Self {
+        Projection::Variable(key_var.into())
+    }
+}
+
+pub trait ProjectionKeyVarBuilder {
+    fn label(self, label: impl Into<ProjectionKeyLabel>) -> ProjectionKeyVar;
+}
+
+impl<T: Into<Variable>> ProjectionKeyVarBuilder for T {
+    fn label(self, label: impl Into<ProjectionKeyLabel>) -> ProjectionKeyVar {
+        let labeled = label.into();
+        ProjectionKeyVar { variable: self.into(), label: Some(labeled) }
+    }
+}
+
+impl<T: Into<Variable>, U: Into<ProjectionKeyLabel>> From<(T, U)> for ProjectionKeyVar {
+    fn from((var, label): (T, U)) -> Self {
+        ProjectionKeyVar { variable: var.into(), label: Some(label.into()) }
+    }
+}
+
+impl<T: Into<Variable>> From<T> for ProjectionKeyVar {
+    fn from(var: T) -> Self {
+        ProjectionKeyVar { variable: var.into(), label: None }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct ProjectionKeyLabel {
+    pub label: String,
+}
+
+impl From<&str> for ProjectionKeyLabel {
+    fn from(value: &str) -> Self {
+        Self::from(value.to_owned())
+    }
+}
+
+impl From<String> for ProjectionKeyLabel {
+    fn from(label: String) -> Self {
+        Self { label }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct ProjectionAttribute {
+    attribute: Type,
+    label: Option<ProjectionKeyLabel>,
+}
+
+impl ProjectionAttribute {
+    pub fn new(attribute: Type, label: Option<ProjectionKeyLabel>) -> Self {
+        Self { attribute, label }
+    }
+
+    pub fn label(self, label: impl Into<ProjectionKeyLabel>) -> Self {
+        ProjectionAttribute { label: Some(label.into()), ..self }
+    }
+}
+
+pub trait ProjectionBuilder {
+    fn map_attribute(self, attribute: impl Into<ProjectionAttribute>) -> Projection;
+    fn map_attributes(self, attribute: Vec<ProjectionAttribute>) -> Projection;
+}
+
+impl<T: Into<ProjectionKeyVar>> ProjectionBuilder for T {
+    fn map_attribute(self, attribute: impl Into<ProjectionAttribute>) -> Projection {
+        Projection::Attribute(self.into(), vec![attribute.into()])
+    }
+
+    fn map_attributes(self, attributes: Vec<ProjectionAttribute>) -> Projection {
+        Projection::Attribute(self.into(), attributes)
+    }
+}
+
+impl Pretty for Fetch {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Fetch)?;
+        for projection in &self.projections {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(projection, indent_level, f)?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Fetch {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{} ", token::Clause::Fetch)?;
+            write_joined!(f, " ", self.projections)?;
+            Ok(())
+        }
+    }
+}
+
+impl Pretty for Projection {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Projection::Variable(key) => {
+                write!(f, "{};", key)
+            }
+            Projection::Attribute(key, attrs) => {
+                write!(f, "{}: ", key)?;
+                write_joined!(f, ", ", attrs)?;
+                write!(f, ";")
+            }
+            Projection::Subquery(label, subquery) => {
+                writeln!(f, "{}: {{", label)?;
+                Pretty::fmt(subquery, indent_level + 1, f)?;
+                writeln!(f)?;
+                indent(indent_level, f)?;
+                write!(f, "}};")?;
+                Ok(())
+            }
+        }
+    }
+}
+
+impl fmt::Display for Projection {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Projection::Variable(key) => {
+                write!(f, "{};", key)
+            }
+            Projection::Attribute(key, attrs) => {
+                write!(f, "{}: ", key)?;
+                write_joined!(f, ", ", attrs)?;
+                write!(f, ";")
+            }
+            Projection::Subquery(label, subquery) => {
+                write!(f, "{}: {{ {} }};", label, subquery)
+            }
+        }
+    }
+}
+
+impl fmt::Display for ProjectionKeyVar {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.variable)?;
+        if let Some(label) = &self.label {
+            write!(f, " {} {}", token::Keyword::As, label)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl fmt::Display for ProjectionKeyLabel {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let label = &self.label;
+        write!(f, "{}", label)
+    }
+}
+
+impl fmt::Display for ProjectionAttribute {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.attribute)?;
+        if let Some(label) = &self.label {
+            write!(f, " {} {}", token::Keyword::As, label)
+        } else {
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/insert.rs b/rust/query/pipeline/stage/insert.rs
new file mode 100644
index 000000000..d4a9fbeef
--- /dev/null
+++ b/rust/query/pipeline/stage/insert.rs
@@ -0,0 +1,52 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    statement::Statement,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Insert {
+    span: Option<Span>,
+    statements: Vec<Statement>,
+}
+
+impl Insert {
+    pub(crate) fn new(span: Option<Span>, statements: Vec<Statement>) -> Self {
+        Self { span, statements }
+    }
+}
+
+impl Pretty for Insert {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Insert)?;
+        for statement in &self.statements {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(statement, indent_level, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Insert {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Insert)?;
+            for statement in &self.statements {
+                write!(f, " {statement};")?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/match_.rs b/rust/query/pipeline/stage/match_.rs
new file mode 100644
index 000000000..fcbad1e9f
--- /dev/null
+++ b/rust/query/pipeline/stage/match_.rs
@@ -0,0 +1,61 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pattern::Pattern,
+    pretty::{indent, Pretty},
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Match {
+    span: Option<Span>,
+    pub patterns: Vec<Pattern>,
+}
+
+impl Match {
+    pub(crate) fn new(span: Option<Span>, patterns: Vec<Pattern>) -> Self {
+        Self { span, patterns }
+    }
+
+    pub fn build(patterns: Vec<Pattern>) -> Self {
+        Self::new(None, patterns)
+    }
+
+    pub fn and(mut self, pattern: Pattern) -> Self {
+        self.patterns.push(pattern);
+        self
+    }
+}
+
+impl Pretty for Match {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Match)?;
+        for pattern in &self.patterns {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(pattern, indent_level, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Match {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Match)?;
+            for pattern in &self.patterns {
+                write!(f, " {pattern};")?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/mod.rs b/rust/query/pipeline/stage/mod.rs
new file mode 100644
index 000000000..cc4263430
--- /dev/null
+++ b/rust/query/pipeline/stage/mod.rs
@@ -0,0 +1,75 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+pub mod delete;
+pub mod fetch;
+mod insert;
+mod match_;
+pub mod modifier;
+mod put;
+pub mod reduce;
+mod update;
+
+use std::fmt;
+
+pub use self::{
+    delete::Delete, fetch::Fetch, insert::Insert, match_::Match, modifier::Modifier, put::Put, reduce::Reduce,
+    update::Update,
+};
+use crate::{pretty::Pretty, util::enum_getter};
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Stage {
+    Match(Match),
+    Insert(Insert),
+    Put(Put),
+    Update(Update),
+    Fetch(Fetch),
+    Delete(Delete),
+    Reduce(Reduce),
+    Modifier(Modifier),
+}
+
+enum_getter! { Stage
+    into_match(Match) => Match,
+    into_insert(Insert) => Insert,
+    into_put(Put) => Put,
+    into_update(Update) => Update,
+    into_fetch(Fetch) => Fetch,
+    into_delete(Delete) => Delete,
+    into_reduce(Reduce) => Reduce,
+    into_modifier(Modifier) => Modifier,
+}
+
+impl Pretty for Stage {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Match(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Insert(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Put(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Update(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Fetch(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Delete(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Reduce(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Modifier(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Stage {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Match(inner) => fmt::Display::fmt(inner, f),
+            Self::Insert(inner) => fmt::Display::fmt(inner, f),
+            Self::Put(inner) => fmt::Display::fmt(inner, f),
+            Self::Update(inner) => fmt::Display::fmt(inner, f),
+            Self::Fetch(inner) => fmt::Display::fmt(inner, f),
+            Self::Delete(inner) => fmt::Display::fmt(inner, f),
+            Self::Reduce(inner) => fmt::Display::fmt(inner, f),
+            Self::Modifier(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/modifier.rs b/rust/query/pipeline/stage/modifier.rs
new file mode 100644
index 000000000..b854e3f6b
--- /dev/null
+++ b/rust/query/pipeline/stage/modifier.rs
@@ -0,0 +1,159 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{
+        token::{self, Order},
+        Span,
+    },
+    pretty::Pretty,
+    util::write_joined,
+    value::Literal,
+    variable::Variable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct OrderedVariable {
+    span: Option<Span>,
+    variable: Variable,
+    ordering: Option<Order>,
+}
+
+impl OrderedVariable {
+    pub fn new(span: Option<Span>, variable: Variable, ordering: Option<Order>) -> Self {
+        Self { span, variable, ordering }
+    }
+}
+
+impl Pretty for OrderedVariable {}
+
+impl fmt::Display for OrderedVariable {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.variable)?;
+        if let Some(ordering) = self.ordering {
+            write!(f, " {}", ordering)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Sort {
+    span: Option<Span>,
+    ordered_variables: Vec<OrderedVariable>,
+}
+
+impl Sort {
+    pub fn new(span: Option<Span>, ordered_variables: Vec<OrderedVariable>) -> Self {
+        Self { span, ordered_variables }
+    }
+}
+
+impl Pretty for Sort {}
+
+impl fmt::Display for Sort {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ", token::Modifier::Sort)?;
+        write_joined!(f, ", ", self.ordered_variables)?;
+        f.write_char(';')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Filter {
+    span: Option<Span>,
+    variables: Vec<Variable>,
+}
+
+impl Filter {
+    pub fn new(span: Option<Span>, variables: Vec<Variable>) -> Self {
+        Self { span, variables }
+    }
+}
+
+impl Pretty for Filter {}
+
+impl fmt::Display for Filter {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ", token::Modifier::Filter)?;
+        write_joined!(f, ", ", self.variables)?;
+        f.write_char(';')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Offset {
+    span: Option<Span>,
+    offset: Literal,
+}
+
+impl Offset {
+    pub fn new(span: Option<Span>, offset: Literal) -> Self {
+        Self { span, offset }
+    }
+}
+
+impl Pretty for Offset {}
+
+impl fmt::Display for Offset {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {};", token::Modifier::Offset, self.offset)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Limit {
+    span: Option<Span>,
+    limit: Literal,
+}
+
+impl Limit {
+    pub fn new(span: Option<Span>, limit: Literal) -> Self {
+        Self { span, limit }
+    }
+}
+
+impl Pretty for Limit {}
+
+impl fmt::Display for Limit {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {};", token::Modifier::Limit, self.limit)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Modifier {
+    Filter(Filter),
+    Sort(Sort),
+    Offset(Offset),
+    Limit(Limit),
+}
+
+impl Pretty for Modifier {
+    fn fmt(&self, indent_level: usize, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            Self::Filter(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Sort(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Offset(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Limit(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Modifier {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Filter(inner) => fmt::Display::fmt(inner, f),
+            Self::Sort(inner) => fmt::Display::fmt(inner, f),
+            Self::Offset(inner) => fmt::Display::fmt(inner, f),
+            Self::Limit(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/put.rs b/rust/query/pipeline/stage/put.rs
new file mode 100644
index 000000000..6aaaafa2b
--- /dev/null
+++ b/rust/query/pipeline/stage/put.rs
@@ -0,0 +1,52 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    statement::Statement,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Put {
+    span: Option<Span>,
+    statements: Vec<Statement>,
+}
+
+impl Put {
+    pub(crate) fn new(span: Option<Span>, statements: Vec<Statement>) -> Self {
+        Self { span, statements }
+    }
+}
+
+impl Pretty for Put {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Put)?;
+        for statement in &self.statements {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(statement, indent_level, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Put {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Put)?;
+            for statement in &self.statements {
+                write!(f, " {statement};")?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/pipeline/stage/reduce.rs b/rust/query/pipeline/stage/reduce.rs
new file mode 100644
index 000000000..ff6e94820
--- /dev/null
+++ b/rust/query/pipeline/stage/reduce.rs
@@ -0,0 +1,158 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pretty::Pretty,
+    util::write_joined,
+    variable::Variable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Reduce {
+    Check(Check),
+    First(First),
+    Value(Vec<ReduceValue>),
+}
+
+impl Pretty for Reduce {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Check(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::First(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Value(inner) => {
+                write_joined!(f, ", ", inner)?;
+                f.write_char(';')?;
+                Ok(())
+            }
+        }
+    }
+}
+
+impl fmt::Display for Reduce {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Check(inner) => fmt::Display::fmt(inner, f),
+            Self::First(inner) => fmt::Display::fmt(inner, f),
+            Self::Value(inner) => {
+                write_joined!(f, ", ", inner)?;
+                Ok(())
+            }
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Check {
+    span: Option<Span>,
+}
+
+impl Check {
+    pub fn new(span: Option<Span>) -> Self {
+        Self { span }
+    }
+}
+
+impl Pretty for Check {}
+
+impl fmt::Display for Check {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{};", token::Aggregate::Check)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct First {
+    span: Option<Span>,
+    variables: Vec<Variable>,
+}
+
+impl First {
+    pub fn new(span: Option<Span>, variables: Vec<Variable>) -> Self {
+        Self { span, variables }
+    }
+}
+
+impl Pretty for First {}
+
+impl fmt::Display for First {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}(", token::Aggregate::First)?;
+        write_joined!(f, ", ", self.variables)?;
+        f.write_str(");")?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum ReduceValue {
+    Count(Count),
+    Stat(Stat),
+}
+
+impl Pretty for ReduceValue {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Count(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Stat(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for ReduceValue {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Count(inner) => fmt::Display::fmt(inner, f),
+            Self::Stat(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Count {
+    span: Option<Span>,
+    variables: Vec<Variable>,
+}
+
+impl Count {
+    pub fn new(span: Option<Span>, variables: Vec<Variable>) -> Self {
+        Self { span, variables }
+    }
+}
+
+impl Pretty for Count {}
+
+impl fmt::Display for Count {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}(", token::Aggregate::Count)?;
+        write_joined!(f, ", ", self.variables)?;
+        f.write_str(")")?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Stat {
+    span: Option<Span>,
+    aggregate: token::Aggregate,
+    variable: Variable,
+}
+
+impl Stat {
+    pub fn new(span: Option<Span>, aggregate: token::Aggregate, variable: Variable) -> Self {
+        Self { span, aggregate, variable }
+    }
+}
+
+impl Pretty for Stat {}
+
+impl fmt::Display for Stat {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}({})", self.aggregate, self.variable)
+    }
+}
diff --git a/rust/query/pipeline/stage/update.rs b/rust/query/pipeline/stage/update.rs
new file mode 100644
index 000000000..e3672cd6f
--- /dev/null
+++ b/rust/query/pipeline/stage/update.rs
@@ -0,0 +1,52 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    statement::Statement,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Update {
+    span: Option<Span>,
+    statements: Vec<Statement>,
+}
+
+impl Update {
+    pub(crate) fn new(span: Option<Span>, statements: Vec<Statement>) -> Self {
+        Self { span, statements }
+    }
+}
+
+impl Pretty for Update {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Clause::Update)?;
+        for statement in &self.statements {
+            writeln!(f)?;
+            indent(indent_level, f)?;
+            Pretty::fmt(statement, indent_level, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Update {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Update)?;
+            for statement in &self.statements {
+                write!(f, " {statement};")?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/schema/define.rs b/rust/query/schema/define.rs
new file mode 100644
index 000000000..b449de598
--- /dev/null
+++ b/rust/query/schema/define.rs
@@ -0,0 +1,62 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span, Spanned},
+    pretty::{indent, Pretty},
+    schema::definable::Definable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Define {
+    span: Option<Span>,
+    definables: Vec<Definable>,
+}
+
+impl Define {
+    pub(crate) fn new(span: Option<Span>, definables: Vec<Definable>) -> Self {
+        Self { span, definables }
+    }
+
+    pub fn build(definables: Vec<Definable>) -> Self {
+        Self::new(None, definables)
+    }
+}
+
+impl Spanned for Define {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Define {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        indent(indent_level, f)?;
+        write!(f, "{}", token::Clause::Define)?;
+        for definable in &self.definables {
+            writeln!(f)?;
+            Pretty::fmt(definable, indent_level + 1, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Define {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}", token::Clause::Define)?;
+            for definable in &self.definables {
+                write!(f, " {};", definable)?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/schema/mod.rs b/rust/query/schema/mod.rs
new file mode 100644
index 000000000..e2b42209d
--- /dev/null
+++ b/rust/query/schema/mod.rs
@@ -0,0 +1,41 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+pub use self::{define::Define, redefine::Redefine, undefine::Undefine};
+use crate::common::{Span, Spanned};
+
+mod define;
+mod redefine;
+mod undefine;
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum SchemaQuery {
+    Define(Define),
+    Redefine(Redefine),
+    Undefine(Undefine),
+}
+
+impl Spanned for SchemaQuery {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Self::Define(inner) => inner.span(),
+            Self::Redefine(inner) => inner.span(),
+            Self::Undefine(inner) => inner.span(),
+        }
+    }
+}
+
+impl fmt::Display for SchemaQuery {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Define(inner) => fmt::Display::fmt(inner, f),
+            Self::Redefine(inner) => fmt::Display::fmt(inner, f),
+            Self::Undefine(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/query/typeql_define.rs b/rust/query/schema/redefine.rs
similarity index 70%
rename from rust/query/typeql_define.rs
rename to rust/query/schema/redefine.rs
index 85c1b980a..75e9d7385 100644
--- a/rust/query/typeql_define.rs
+++ b/rust/query/schema/redefine.rs
@@ -8,35 +8,35 @@ use std::fmt::{self, Write};
 
 use crate::{
     common::{token, Span, Spanned},
-    pattern::Definable,
-    write_joined,
+    schema::definable::Definable,
+    util::write_joined,
 };
 
 #[derive(Debug, Eq, PartialEq)]
-pub struct TypeQLDefine {
-    definables: Vec<Definable>,
+pub struct Redefine {
     span: Option<Span>,
+    definables: Vec<Definable>,
 }
 
-impl TypeQLDefine {
-    pub(crate) fn new(definables: Vec<Definable>, span: Option<Span>) -> Self {
-        Self { definables, span }
+impl Redefine {
+    pub(crate) fn new(span: Option<Span>, definables: Vec<Definable>) -> Self {
+        Self { span, definables }
     }
 
     pub fn build(definables: Vec<Definable>) -> Self {
-        Self::new(definables, None)
+        Self::new(None, definables)
     }
 }
 
-impl Spanned for TypeQLDefine {
+impl Spanned for Redefine {
     fn span(&self) -> Option<Span> {
         self.span
     }
 }
 
-impl fmt::Display for TypeQLDefine {
+impl fmt::Display for Redefine {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        token::Clause::Define.fmt(f)?;
+        write!(f, "{}", token::Clause::Redefine)?;
         if f.alternate() {
             f.write_char('\n')?;
         } else {
diff --git a/rust/query/schema/undefine.rs b/rust/query/schema/undefine.rs
new file mode 100644
index 000000000..192cf04d9
--- /dev/null
+++ b/rust/query/schema/undefine.rs
@@ -0,0 +1,62 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{token, Span, Spanned},
+    pretty::{indent, Pretty},
+    schema::undefinable::Undefinable,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Undefine {
+    span: Option<Span>,
+    undefinables: Vec<Undefinable>,
+}
+
+impl Undefine {
+    pub(crate) fn new(span: Option<Span>, undefinables: Vec<Undefinable>) -> Self {
+        Self { span, undefinables }
+    }
+
+    pub fn build(definables: Vec<Undefinable>) -> Self {
+        Self::new(None, definables)
+    }
+}
+
+impl Spanned for Undefine {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Undefine {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        indent(indent_level, f)?;
+        write!(f, "{}", token::Clause::Undefine)?;
+        for undefinable in &self.undefinables {
+            writeln!(f)?;
+            Pretty::fmt(undefinable, indent_level + 1, f)?;
+            f.write_char(';')?;
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Undefine {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            token::Clause::Undefine.fmt(f)?;
+            for undefinable in &self.undefinables {
+                write!(f, " {};", undefinable)?;
+            }
+            Ok(())
+        }
+    }
+}
diff --git a/rust/query/typeql_delete.rs b/rust/query/typeql_delete.rs
deleted file mode 100644
index 7e484675f..000000000
--- a/rust/query/typeql_delete.rs
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::{ThingStatement, VariablesRetrieved},
-    query::{modifier::Modifiers, writable::validate_non_empty, MatchClause, Sorting, TypeQLUpdate, Writable},
-    write_joined,
-};
-use crate::variable::variable::VariableRef;
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct TypeQLDelete {
-    pub match_clause: MatchClause,
-    pub statements: Vec<ThingStatement>,
-    pub modifiers: Modifiers,
-}
-
-impl TypeQLDelete {
-    pub fn insert(self, writable: impl Writable) -> TypeQLUpdate {
-        TypeQLUpdate { query_delete: self, insert_statements: writable.statements(), modifiers: Default::default() }
-    }
-
-    pub fn sort(self, sorting: impl Into<Sorting>) -> Self {
-        TypeQLDelete { modifiers: self.modifiers.sort(sorting), ..self }
-    }
-
-    pub fn limit(self, limit: usize) -> Self {
-        TypeQLDelete { modifiers: self.modifiers.limit(limit), ..self }
-    }
-
-    pub fn offset(self, offset: usize) -> Self {
-        TypeQLDelete { modifiers: self.modifiers.offset(offset), ..self }
-    }
-}
-
-impl Validatable for TypeQLDelete {
-    fn validate(&self) -> Result {
-        let match_variables = self.match_clause.retrieved_variables().collect();
-        collect_err(
-            ([
-                self.match_clause.validate(),
-                validate_delete_in_scope(&match_variables, &self.statements),
-                validate_non_empty(&self.statements),
-                self.modifiers.sorting.as_ref().map(|s| s.validate(&match_variables)).unwrap_or(Ok(())),
-            ]
-            .into_iter())
-            .chain(self.statements.iter().map(Validatable::validate)),
-        )
-    }
-}
-
-fn validate_delete_in_scope(scope_variables: &HashSet<VariableRef<'_>>, statements: &[ThingStatement]) -> Result {
-    collect_err(statements.iter().flat_map(|v| v.variables()).filter(|r| r.is_name()).map(|r| -> Result {
-        if scope_variables.contains(&r) {
-            Ok(())
-        } else {
-            Err(TypeQLError::DeleteVarNotBound { variable: r.to_owned() })?
-        }
-    }))
-}
-
-impl fmt::Display for TypeQLDelete {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(f, "{}", self.match_clause)?;
-        writeln!(f, "{}", token::Clause::Delete)?;
-        write_joined!(f, ";\n", self.statements)?;
-        f.write_str(";")?;
-        if !self.modifiers.is_empty() {
-            write!(f, "\n{}", self.modifiers)
-        } else {
-            Ok(())
-        }
-    }
-}
diff --git a/rust/query/typeql_fetch.rs b/rust/query/typeql_fetch.rs
deleted file mode 100644
index 8cfd0214f..000000000
--- a/rust/query/typeql_fetch.rs
+++ /dev/null
@@ -1,282 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt, iter};
-
-use itertools::Itertools;
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        identifier::is_valid_label_identifier,
-        string::indent,
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::{Label, VariablesRetrieved},
-    query::{modifier::Modifiers, MatchClause, TypeQLGetAggregate},
-    write_joined,
-};
-use crate::variable::Variable;
-use crate::variable::variable::VariableRef;
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct TypeQLFetch {
-    pub match_clause: MatchClause,
-    pub projections: Vec<Projection>,
-    pub modifiers: Modifiers,
-}
-
-impl Validatable for TypeQLFetch {
-    fn validate(&self) -> Result {
-        let match_variables = self.match_clause.retrieved_variables().collect();
-        collect_err([
-            self.match_clause.validate(),
-            self.modifiers.sorting.as_ref().map_or(Ok(()), |s| s.validate(&match_variables)),
-        ])
-    }
-}
-
-impl VariablesRetrieved for TypeQLFetch {
-    fn retrieved_variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        Box::new(
-            self.match_clause.retrieved_variables().chain(self.projections.iter().flat_map(Projection::key_variable)),
-        )
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub enum Projection {
-    Variable(ProjectionKeyVar),
-    Attribute(ProjectionKeyVar, Vec<ProjectionAttribute>),
-    Subquery(ProjectionKeyLabel, ProjectionSubquery),
-}
-
-impl Projection {
-    pub fn key_variable(&self) -> Option<VariableRef<'_>> {
-        match self {
-            Projection::Variable(key) | Projection::Attribute(key, _) => Some(key.variable.as_ref()),
-            Projection::Subquery(_, _) => None,
-        }
-    }
-
-    pub fn value_variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        match self {
-            Projection::Variable(_) | Projection::Attribute(_, _) => Box::new(iter::empty()),
-            Projection::Subquery(_, subquery) => subquery.variables(),
-        }
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct ProjectionKeyVar {
-    pub(crate) variable: Variable,
-    pub(crate) label: Option<ProjectionKeyLabel>,
-}
-
-impl ProjectionKeyVar {
-    pub fn label(self, label: impl Into<ProjectionKeyLabel>) -> Self {
-        ProjectionKeyVar { label: Some(label.into()), ..self }
-    }
-}
-
-impl<T: Into<ProjectionKeyVar>> From<T> for Projection {
-    fn from(key_var: T) -> Self {
-        Projection::Variable(key_var.into())
-    }
-}
-
-pub trait ProjectionKeyVarBuilder {
-    fn label(self, label: impl Into<ProjectionKeyLabel>) -> ProjectionKeyVar;
-}
-
-impl<T: Into<Variable>> ProjectionKeyVarBuilder for T {
-    fn label(self, label: impl Into<ProjectionKeyLabel>) -> ProjectionKeyVar {
-        let labeled = label.into();
-        ProjectionKeyVar { variable: self.into(), label: Some(labeled) }
-    }
-}
-
-impl<T: Into<Variable>, U: Into<ProjectionKeyLabel>> From<(T, U)> for ProjectionKeyVar {
-    fn from((var, label): (T, U)) -> Self {
-        ProjectionKeyVar { variable: var.into(), label: Some(label.into()) }
-    }
-}
-
-impl<T: Into<Variable>> From<T> for ProjectionKeyVar {
-    fn from(var: T) -> Self {
-        ProjectionKeyVar { variable: var.into(), label: None }
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct ProjectionKeyLabel {
-    pub label: String,
-}
-
-impl ProjectionKeyLabel {
-    pub fn map_subquery_get_aggregate(self, subquery: TypeQLGetAggregate) -> Projection {
-        Projection::Subquery(self, ProjectionSubquery::GetAggregate(subquery))
-    }
-
-    pub fn map_subquery_fetch(self, subquery: TypeQLFetch) -> Projection {
-        Projection::Subquery(self, ProjectionSubquery::Fetch(Box::new(subquery)))
-    }
-
-    fn must_quote(s: &str) -> bool {
-        !is_valid_label_identifier(s)
-    }
-}
-
-impl From<&str> for ProjectionKeyLabel {
-    fn from(value: &str) -> Self {
-        Self::from(value.to_owned())
-    }
-}
-
-impl From<String> for ProjectionKeyLabel {
-    fn from(label: String) -> Self {
-        Self { label }
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct ProjectionAttribute {
-    pub(crate) attribute: Label,
-    pub(crate) label: Option<ProjectionKeyLabel>,
-}
-
-impl ProjectionAttribute {
-    pub fn label(self, label: impl Into<ProjectionKeyLabel>) -> Self {
-        ProjectionAttribute { label: Some(label.into()), ..self }
-    }
-}
-
-impl From<&str> for ProjectionAttribute {
-    fn from(attribute: &str) -> Self {
-        Self::from(attribute.to_owned())
-    }
-}
-
-impl From<String> for ProjectionAttribute {
-    fn from(attribute: String) -> Self {
-        ProjectionAttribute { attribute: Label::from(attribute), label: None }
-    }
-}
-
-impl<T: Into<Label>, U: Into<ProjectionKeyLabel>> From<(T, U)> for ProjectionAttribute {
-    fn from((attribute, label): (T, U)) -> Self {
-        Self { attribute: attribute.into(), label: Some(label.into()) }
-    }
-}
-
-#[derive(Debug, Eq, PartialEq)]
-pub enum ProjectionSubquery {
-    GetAggregate(TypeQLGetAggregate),
-    Fetch(Box<TypeQLFetch>),
-}
-
-impl ProjectionSubquery {
-    pub fn variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        match self {
-            ProjectionSubquery::GetAggregate(query) => query.query.retrieved_variables(),
-            ProjectionSubquery::Fetch(query) => query.retrieved_variables(),
-        }
-    }
-}
-
-pub trait ProjectionBuilder {
-    fn map_attribute(self, attribute: impl Into<ProjectionAttribute>) -> Projection;
-    fn map_attributes(self, attribute: Vec<ProjectionAttribute>) -> Projection;
-}
-
-impl<T: Into<ProjectionKeyVar>> ProjectionBuilder for T {
-    fn map_attribute(self, attribute: impl Into<ProjectionAttribute>) -> Projection {
-        Projection::Attribute(self.into(), vec![attribute.into()])
-    }
-
-    fn map_attributes(self, attributes: Vec<ProjectionAttribute>) -> Projection {
-        Projection::Attribute(self.into(), attributes)
-    }
-}
-
-impl fmt::Display for TypeQLFetch {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(f, "{}", self.match_clause)?;
-        writeln!(f, "{}", token::Clause::Fetch)?;
-        write_joined!(f, "\n", self.projections)?;
-        if !self.modifiers.is_empty() {
-            write!(f, "\n{}", self.modifiers)
-        } else {
-            Ok(())
-        }
-    }
-}
-
-impl fmt::Display for Projection {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Projection::Variable(key) => {
-                write!(f, "{};", key)
-            }
-            Projection::Attribute(key, attrs) => {
-                write!(f, "{}: ", key)?;
-                write_joined!(f, ", ", attrs)?;
-                write!(f, ";")
-            }
-            Projection::Subquery(label, subquery) => {
-                write!(f, "{}: {{\n{}\n}};", label, indent(subquery.to_string().as_ref()))
-            }
-        }
-    }
-}
-
-impl fmt::Display for ProjectionKeyVar {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.variable)?;
-        if let Some(label) = &self.label {
-            write!(f, " {} {}", token::Projection::As, label)
-        } else {
-            Ok(())
-        }
-    }
-}
-
-impl fmt::Display for ProjectionKeyLabel {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let label = &self.label;
-        if Self::must_quote(label) {
-            write!(f, "\"{}\"", label)
-        } else {
-            write!(f, "{}", label)
-        }
-    }
-}
-
-impl fmt::Display for ProjectionAttribute {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.attribute)?;
-        if let Some(label) = &self.label {
-            write!(f, " {} {}", token::Projection::As, label)
-        } else {
-            Ok(())
-        }
-    }
-}
-
-impl fmt::Display for ProjectionSubquery {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            ProjectionSubquery::GetAggregate(query) => {
-                write!(f, "{}", query)
-            }
-            ProjectionSubquery::Fetch(query) => {
-                write!(f, "{}", query)
-            }
-        }
-    }
-}
diff --git a/rust/query/typeql_get.rs b/rust/query/typeql_get.rs
deleted file mode 100644
index 3a6b7815d..000000000
--- a/rust/query/typeql_get.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt};
-
-use itertools::Itertools;
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::{Conjunction, VariablesRetrieved},
-    query::{
-        modifier::{Modifiers, Sorting},
-        AggregateQueryBuilder, MatchClause, TypeQLGetGroup,
-    },
-    write_joined,
-};
-use crate::variable::Variable;
-use crate::variable::variable::VariableRef;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TypeQLGet {
-    pub match_clause: MatchClause,
-    pub filter: Filter,
-    pub modifiers: Modifiers,
-}
-
-impl AggregateQueryBuilder for TypeQLGet {}
-
-impl TypeQLGet {
-    pub fn new(match_clause: MatchClause) -> Self {
-        TypeQLGet { match_clause, filter: Filter::default(), modifiers: Modifiers::default() }
-    }
-
-    pub fn sort(self, sorting: impl Into<Sorting>) -> Self {
-        TypeQLGet { modifiers: self.modifiers.sort(sorting), ..self }
-    }
-
-    pub fn limit(self, limit: usize) -> Self {
-        TypeQLGet { modifiers: self.modifiers.limit(limit), ..self }
-    }
-
-    pub fn offset(self, offset: usize) -> Self {
-        TypeQLGet { modifiers: self.modifiers.offset(offset), ..self }
-    }
-
-    pub fn group(self, var: impl Into<Variable>) -> TypeQLGetGroup {
-        TypeQLGetGroup { query: self, group_var: var.into() }
-    }
-}
-
-impl Validatable for TypeQLGet {
-    fn validate(&self) -> Result {
-        let match_variables = self.match_clause.retrieved_variables().collect();
-        let filter_vars = HashSet::from_iter(self.filter.vars.iter().map(Variable::as_ref));
-        let retrieved_variables = if self.filter.vars.is_empty() { &match_variables } else { &filter_vars };
-        collect_err([
-            self.match_clause.validate(),
-            validate_filters_are_in_scope(&match_variables, &self.filter),
-            self.modifiers.sorting.as_ref().map(|s| s.validate(retrieved_variables)).unwrap_or(Ok(())),
-        ])
-    }
-}
-
-impl VariablesRetrieved for TypeQLGet {
-    fn retrieved_variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        if !self.filter.vars.is_empty() {
-            Box::new(self.filter.vars.iter().map(Variable::as_ref))
-        } else {
-            self.match_clause.retrieved_variables()
-        }
-    }
-}
-
-fn validate_filters_are_in_scope(match_variables: &HashSet<VariableRef<'_>>, filter: &Filter) -> Result {
-    let mut seen = HashSet::new();
-    collect_err(filter.vars.iter().map(|variable| {
-        if !variable.is_named() {
-            Err(TypeQLError::VariableNotNamed.into())
-        } else if !match_variables.contains(&variable.as_ref()) {
-            Err(TypeQLError::GetVarNotBound { variable: variable.clone() }.into())
-        } else if seen.contains(&variable) {
-            Err(TypeQLError::GetVarRepeating { variable: variable.clone() }.into())
-        } else {
-            seen.insert(variable);
-            Ok(())
-        }
-    }))
-}
-
-impl fmt::Display for TypeQLGet {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.match_clause)?;
-        write!(f, "\n{}", self.filter)?;
-        if !self.modifiers.is_empty() {
-            write!(f, "\n{}", self.modifiers)
-        } else {
-            Ok(())
-        }
-    }
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Default)]
-pub struct Filter {
-    pub vars: Vec<Variable>,
-}
-
-impl fmt::Display for Filter {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", token::Clause::Get)?;
-        if !self.vars.is_empty() {
-            write!(f, " ")?;
-            write_joined!(f, ", ", self.vars)?;
-        }
-        write!(f, ";")
-    }
-}
diff --git a/rust/query/typeql_get_aggregate.rs b/rust/query/typeql_get_aggregate.rs
deleted file mode 100644
index c60888023..000000000
--- a/rust/query/typeql_get_aggregate.rs
+++ /dev/null
@@ -1,124 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::VariablesRetrieved,
-    query::{TypeQLGet, TypeQLGetGroup},
-};
-use crate::variable::Variable;
-use crate::variable::variable::VariableRef;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct AggregateQuery<T>
-where
-    T: AggregateQueryBuilder,
-{
-    pub query: T,
-    pub method: token::Aggregate,
-    pub var: Option<Variable>,
-}
-
-pub type TypeQLGetAggregate = AggregateQuery<TypeQLGet>;
-pub type TypeQLGetGroupAggregate = AggregateQuery<TypeQLGetGroup>;
-
-impl<T: AggregateQueryBuilder> AggregateQuery<T> {
-    fn new_count(query: T) -> Self {
-        Self { query, method: token::Aggregate::Count, var: None }
-    }
-
-    fn new(query: T, method: token::Aggregate, var: impl Into<Variable>) -> Self {
-        Self { query, method, var: Some(var.into()) }
-    }
-}
-
-impl<T: AggregateQueryBuilder> Validatable for AggregateQuery<T> {
-    fn validate(&self) -> Result {
-        let retrieved_variables = self.query.retrieved_variables().collect();
-        collect_err(
-            [self.query.validate(), validate_method_variable_compatible(&self.method, &self.var)]
-                .into_iter()
-                .chain(self.var.iter().map(|v| validate_variable_in_scope(v, &retrieved_variables))),
-        )
-    }
-}
-
-fn validate_method_variable_compatible(method: &token::Aggregate, var: &Option<Variable>) -> Result {
-    if *method == token::Aggregate::Count && var.is_some() {
-        Err(TypeQLError::InvalidCountVariableArgument)?
-    }
-    Ok(())
-}
-
-fn validate_variable_in_scope(var: &Variable, scope_variables: &HashSet<VariableRef<'_>>) -> Result {
-    if !scope_variables.contains(&var.as_ref()) {
-        Err(TypeQLError::AggregateVarNotBound { variable: var.clone() })?;
-    }
-    Ok(())
-}
-
-impl fmt::Display for TypeQLGetAggregate {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}\n{}", self.query, self.method)?;
-        if let Some(var) = &self.var {
-            write!(f, " {var}")?;
-        }
-        f.write_str(";")
-    }
-}
-
-impl fmt::Display for TypeQLGetGroupAggregate {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{} {}", self.query, self.method)?;
-        if let Some(var) = &self.var {
-            write!(f, " {var}")?;
-        }
-        f.write_str(";")
-    }
-}
-
-pub trait AggregateQueryBuilder:
-    Sized + Clone + fmt::Display + fmt::Debug + Eq + PartialEq + VariablesRetrieved + Validatable
-{
-    fn count(self) -> AggregateQuery<Self> {
-        AggregateQuery::<Self>::new_count(self)
-    }
-
-    fn aggregate(self, method: token::Aggregate, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        AggregateQuery::<Self>::new(self, method, var)
-    }
-
-    fn max(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Max, var.into())
-    }
-
-    fn min(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Min, var.into())
-    }
-
-    fn mean(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Mean, var.into())
-    }
-
-    fn median(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Median, var.into())
-    }
-
-    fn std(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Std, var.into())
-    }
-
-    fn sum(self, var: impl Into<Variable>) -> AggregateQuery<Self> {
-        self.aggregate(token::Aggregate::Sum, var.into())
-    }
-}
diff --git a/rust/query/typeql_get_group.rs b/rust/query/typeql_get_group.rs
deleted file mode 100644
index e648124db..000000000
--- a/rust/query/typeql_get_group.rs
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt, iter};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::VariablesRetrieved,
-    query::{AggregateQueryBuilder, TypeQLGet},
-};
-use crate::variable::Variable;
-use crate::variable::variable::VariableRef;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TypeQLGetGroup {
-    pub query: TypeQLGet,
-    pub group_var: Variable,
-}
-
-impl AggregateQueryBuilder for TypeQLGetGroup {}
-
-impl Validatable for TypeQLGetGroup {
-    fn validate(&self) -> Result {
-        let retrieved_variables = self.query.retrieved_variables().collect();
-        collect_err(
-            [self.query.validate(), self.group_var.validate()]
-                .into_iter()
-                .chain(iter::once(&self.group_var).map(|v| validate_variable_in_scope(v, &retrieved_variables))),
-        )
-    }
-}
-
-impl VariablesRetrieved for TypeQLGetGroup {
-    fn retrieved_variables(&self) -> Box<dyn Iterator<Item = VariableRef<'_>> + '_> {
-        self.query.retrieved_variables()
-    }
-}
-
-fn validate_variable_in_scope(var: &Variable, scope_variables: &HashSet<VariableRef<'_>>) -> Result {
-    if !scope_variables.contains(&var.as_ref()) {
-        Err(TypeQLError::GroupVarNotBound { variable: var.clone() })?;
-    }
-    Ok(())
-}
-
-impl fmt::Display for TypeQLGetGroup {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}\n{} {};", self.query, token::Clause::Group, self.group_var)
-    }
-}
diff --git a/rust/query/typeql_insert.rs b/rust/query/typeql_insert.rs
deleted file mode 100644
index 9f0227e23..000000000
--- a/rust/query/typeql_insert.rs
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{collections::HashSet, fmt};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::{ThingStatement, VariablesRetrieved},
-    query::{modifier::Modifiers, writable::validate_non_empty, MatchClause, Sorting},
-    write_joined,
-};
-use crate::variable::variable::VariableRef;
-
-#[derive(Clone, Debug, Eq, PartialEq)]
-pub struct TypeQLInsert {
-    pub match_clause: Option<MatchClause>,
-    pub statements: Vec<ThingStatement>,
-    pub modifiers: Modifiers,
-}
-
-impl TypeQLInsert {
-    pub fn new(statements: Vec<ThingStatement>) -> Self {
-        TypeQLInsert { match_clause: None, statements, modifiers: Modifiers::default() }
-    }
-
-    pub fn sort(self, sorting: impl Into<Sorting>) -> Self {
-        TypeQLInsert { modifiers: self.modifiers.sort(sorting), ..self }
-    }
-
-    pub fn limit(self, limit: usize) -> Self {
-        TypeQLInsert { modifiers: self.modifiers.limit(limit), ..self }
-    }
-
-    pub fn offset(self, offset: usize) -> Self {
-        TypeQLInsert { modifiers: self.modifiers.offset(offset), ..self }
-    }
-
-    fn validate_modifiers_have_match_clause(&self) -> Result {
-        if !self.modifiers.is_empty() && self.match_clause.is_none() {
-            Err(TypeQLError::InsertModifiersRequireMatch { insert: self.to_string() })?
-        } else {
-            Ok(())
-        }
-    }
-}
-
-impl Validatable for TypeQLInsert {
-    fn validate(&self) -> Result {
-        collect_err(
-            [
-                validate_non_empty(&self.statements),
-                self.validate_modifiers_have_match_clause(),
-                self.match_clause
-                    .as_ref()
-                    .map(|m| {
-                        m.validate()?;
-                        let match_variables = m.retrieved_variables().collect();
-                        validate_insert_in_scope_of_match(&match_variables, &self.statements)
-                    })
-                    .unwrap_or_else(|| Ok(())),
-            ]
-            .into_iter()
-            .chain(self.statements.iter().map(Validatable::validate)),
-        )
-    }
-}
-
-fn validate_insert_in_scope_of_match(
-    match_variables: &HashSet<VariableRef<'_>>,
-    statements: &[ThingStatement],
-) -> Result {
-    if statements.iter().flat_map(|s| s.variables()).any(|v| match_variables.contains(&v)) {
-        Ok(())
-    } else {
-        let stmts_str = statements.iter().map(ThingStatement::to_string).collect::<Vec<String>>().join(", ");
-        let bounds_str = match_variables.iter().map(VariableRef::to_string).collect::<Vec<String>>().join(", ");
-        Err(TypeQLError::InsertClauseNotBound { insert_statements: stmts_str, bounds: bounds_str })?
-    }
-}
-
-impl fmt::Display for TypeQLInsert {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if let Some(match_clause) = &self.match_clause {
-            writeln!(f, "{match_clause}")?;
-        }
-
-        writeln!(f, "{}", token::Clause::Insert)?;
-        write_joined!(f, ";\n", self.statements)?;
-        f.write_str(";")?;
-        if !self.modifiers.is_empty() {
-            write!(f, "\n{}", self.modifiers)
-        } else {
-            Ok(())
-        }
-    }
-}
diff --git a/rust/query/typeql_undefine.rs b/rust/query/typeql_undefine.rs
deleted file mode 100644
index 144fb1826..000000000
--- a/rust/query/typeql_undefine.rs
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::{fmt, iter};
-
-use crate::{
-    common::{
-        error::{collect_err, TypeQLError},
-        token,
-        validatable::Validatable,
-        Result,
-    },
-    pattern::{Definable, RuleLabel, TypeStatement},
-    write_joined,
-};
-
-#[derive(Debug, Default, Eq, PartialEq)]
-pub struct TypeQLUndefine {
-    statements: Vec<TypeStatement>,
-    rules: Vec<RuleLabel>,
-}
-
-impl TypeQLUndefine {
-    pub fn new(undefinables: Vec<Definable>) -> Self {
-        undefinables.into_iter().fold(TypeQLUndefine::default(), |undefine, undefinable| match undefinable {
-            Definable::RuleDeclaration(rule) => undefine.add_rule(rule),
-            Definable::TypeStatement(var) => undefine.add_statement(var),
-            Definable::RuleDefinition(rule) => {
-                panic!("{}", TypeQLError::InvalidUndefineQueryRule { rule_label: rule.label })
-            }
-        })
-    }
-
-    fn add_statement(mut self, statement: TypeStatement) -> Self {
-        self.statements.push(statement);
-        self
-    }
-
-    fn add_rule(mut self, rule: RuleLabel) -> Self {
-        self.rules.push(rule);
-        self
-    }
-
-    fn validate_non_empty(&self) -> Result {
-        if self.statements.is_empty() && self.rules.is_empty() {
-            Err(TypeQLError::MissingDefinables)?
-        }
-        Ok(())
-    }
-}
-
-impl Validatable for TypeQLUndefine {
-    fn validate(&self) -> Result {
-        collect_err(
-            &mut iter::once(self.validate_non_empty())
-                .chain(self.statements.iter().map(Validatable::validate))
-                .chain(self.statements.iter().map(TypeStatement::validate_definable))
-                .chain(self.rules.iter().map(Validatable::validate)),
-        )
-    }
-}
-
-impl fmt::Display for TypeQLUndefine {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(f, "{}", token::Clause::Undefine)?;
-        write_joined!(f, ";\n", self.statements, self.rules)?;
-        f.write_str(";")
-    }
-}
diff --git a/rust/query/typeql_update.rs b/rust/query/typeql_update.rs
deleted file mode 100644
index f652bbde8..000000000
--- a/rust/query/typeql_update.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use std::fmt;
-
-use crate::{
-    common::{error::collect_err, token, validatable::Validatable, Result},
-    pattern::{ThingStatement, VariablesRetrieved},
-    query::{modifier::Modifiers, writable::validate_non_empty, Sorting, TypeQLDelete},
-    write_joined,
-};
-
-#[derive(Debug, Eq, PartialEq)]
-pub struct TypeQLUpdate {
-    pub query_delete: TypeQLDelete,
-    pub insert_statements: Vec<ThingStatement>,
-    pub modifiers: Modifiers,
-}
-
-impl TypeQLUpdate {
-    pub fn sort(self, sorting: impl Into<Sorting>) -> Self {
-        TypeQLUpdate { modifiers: self.modifiers.sort(sorting), ..self }
-    }
-
-    pub fn limit(self, limit: usize) -> Self {
-        TypeQLUpdate { modifiers: self.modifiers.limit(limit), ..self }
-    }
-
-    pub fn offset(self, offset: usize) -> Self {
-        TypeQLUpdate { modifiers: self.modifiers.offset(offset), ..self }
-    }
-}
-
-impl Validatable for TypeQLUpdate {
-    fn validate(&self) -> Result {
-        let match_variables = self.query_delete.match_clause.retrieved_variables().collect();
-        collect_err(
-            [
-                validate_non_empty(&self.insert_statements),
-                self.query_delete.validate(),
-                self.modifiers.sorting.as_ref().map(|s| s.validate(&match_variables)).unwrap_or(Ok(())),
-            ]
-            .into_iter()
-            .chain(self.insert_statements.iter().map(Validatable::validate)),
-        )
-    }
-}
-
-impl fmt::Display for TypeQLUpdate {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        writeln!(f, "{}", self.query_delete)?;
-        writeln!(f, "{}", token::Clause::Insert)?;
-        write_joined!(f, ";\n", self.insert_statements)?;
-        f.write_str(";")?;
-        if !self.modifiers.is_empty() {
-            write!(f, "\n{}", self.modifiers)
-        } else {
-            Ok(())
-        }
-    }
-}
diff --git a/rust/query/writable.rs b/rust/query/writable.rs
deleted file mode 100644
index 03077a45b..000000000
--- a/rust/query/writable.rs
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
- */
-
-use crate::{common::error::TypeQLError, pattern::ThingStatement, Result};
-
-pub trait Writable {
-    fn statements(self) -> Vec<ThingStatement>;
-}
-
-impl Writable for ThingStatement {
-    fn statements(self) -> Vec<ThingStatement> {
-        vec![self]
-    }
-}
-
-impl<const N: usize> Writable for [ThingStatement; N] {
-    fn statements(self) -> Vec<ThingStatement> {
-        self.to_vec()
-    }
-}
-
-impl Writable for Vec<ThingStatement> {
-    fn statements(self) -> Vec<ThingStatement> {
-        self
-    }
-}
-
-pub(crate) fn validate_non_empty(statements: &[ThingStatement]) -> Result {
-    if statements.is_empty() {
-        Err(TypeQLError::MissingPatterns)?
-    }
-    Ok(())
-}
diff --git a/rust/schema/definable/function.rs b/rust/schema/definable/function.rs
new file mode 100644
index 000000000..b589f6410
--- /dev/null
+++ b/rust/schema/definable/function.rs
@@ -0,0 +1,235 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    common::{identifier::Identifier, token, Span, Spanned},
+    pretty::{indent, Pretty},
+    query::pipeline::stage::{Match, Modifier, Reduce},
+    type_::TypeAny,
+    variable::Variable,
+};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Function {
+    span: Option<Span>,
+    signature: Signature,
+    body: Match,
+    modifiers: Vec<Modifier>,
+    return_stmt: ReturnStatement,
+}
+
+impl Function {
+    pub fn new(
+        span: Option<Span>,
+        signature: Signature,
+        body: Match,
+        modifiers: Vec<Modifier>,
+        return_stmt: ReturnStatement,
+    ) -> Self {
+        Self { span, signature, body, modifiers, return_stmt }
+    }
+}
+
+impl Pretty for Function {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        indent(indent_level, f)?;
+        write!(f, "{} ", token::Keyword::Fun)?;
+        Pretty::fmt(&self.signature, indent_level, f)?;
+        f.write_char(':')?;
+        Ok(())
+    }
+}
+
+impl fmt::Display for Function {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            return Pretty::fmt(self, 0, f);
+        }
+
+        todo!()
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Signature {
+    span: Option<Span>,
+    ident: Identifier,
+    args: Vec<Argument>,
+    output: Output,
+}
+
+impl Signature {
+    pub(crate) fn new(span: Option<Span>, ident: Identifier, args: Vec<Argument>, output: Output) -> Self {
+        Self { span, ident, args, output }
+    }
+}
+
+impl Pretty for Signature {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}(", self.ident)?;
+        todo!("write args");
+        write!(f, ") -> ")?;
+        Pretty::fmt(&self.output, indent_level, f)?;
+        Ok(())
+    }
+}
+
+impl fmt::Display for Signature {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            Pretty::fmt(self, 0, f)
+        } else {
+            write!(f, "{}(", self.ident)?;
+            todo!("write args");
+            write!(f, ") -> {}", self.output)?;
+            Ok(())
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Argument {
+    span: Option<Span>,
+    var: Variable,
+    type_: TypeAny,
+}
+
+impl Argument {
+    pub fn new(span: Option<Span>, var: Variable, type_: TypeAny) -> Self {
+        Self { span, var, type_ }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum Output {
+    Stream(Stream),
+    Single(Single),
+}
+
+impl Spanned for Output {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Self::Stream(inner) => inner.span(),
+            Self::Single(inner) => inner.span(),
+        }
+    }
+}
+
+impl Pretty for Output {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Stream(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Single(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Output {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Stream(inner) => fmt::Display::fmt(inner, f),
+            Self::Single(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Stream {
+    span: Option<Span>,
+    types: Vec<TypeAny>,
+}
+
+impl Stream {
+    pub fn new(span: Option<Span>, types: Vec<TypeAny>) -> Self {
+        Self { span, types }
+    }
+}
+
+impl Spanned for Stream {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Stream {
+    fn fmt(&self, _indent_level: usize, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+impl fmt::Display for Stream {
+    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Single {
+    span: Option<Span>,
+    types: Vec<TypeAny>,
+}
+
+impl Single {
+    pub fn new(span: Option<Span>, types: Vec<TypeAny>) -> Self {
+        Self { span, types }
+    }
+}
+
+impl Spanned for Single {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Single {
+    fn fmt(&self, _indent_level: usize, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+impl fmt::Display for Single {
+    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ReturnStream {
+    span: Option<Span>,
+    vars: Vec<Variable>,
+}
+
+impl ReturnStream {
+    pub fn new(span: Option<Span>, vars: Vec<Variable>) -> Self {
+        Self { span, vars }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum SingleOutput {
+    Variable(Variable),
+    Reduce(Reduce),
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct ReturnSingle {
+    span: Option<Span>,
+    outputs: Vec<SingleOutput>,
+}
+
+impl ReturnSingle {
+    pub fn new(span: Option<Span>, outputs: Vec<SingleOutput>) -> Self {
+        Self { span, outputs }
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub enum ReturnStatement {
+    Stream(ReturnStream),
+    Single(ReturnSingle),
+}
diff --git a/rust/schema/definable/mod.rs b/rust/schema/definable/mod.rs
new file mode 100644
index 000000000..cde14f4a3
--- /dev/null
+++ b/rust/schema/definable/mod.rs
@@ -0,0 +1,47 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+pub use self::{function::Function, struct_::Struct, type_::Type};
+use crate::pretty::Pretty;
+
+pub mod function;
+pub mod struct_;
+pub mod type_;
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Definable {
+    TypeDeclaration(Type),
+    Function(Function),
+    Struct(Struct),
+}
+
+impl From<Type> for Definable {
+    fn from(type_: Type) -> Self {
+        Self::TypeDeclaration(type_)
+    }
+}
+
+impl Pretty for Definable {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::TypeDeclaration(declaration) => Pretty::fmt(declaration, indent_level, f),
+            Self::Function(declaration) => Pretty::fmt(declaration, indent_level, f),
+            Self::Struct(declaration) => Pretty::fmt(declaration, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Definable {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::TypeDeclaration(declaration) => fmt::Display::fmt(declaration, f),
+            Self::Function(declaration) => fmt::Display::fmt(declaration, f),
+            Self::Struct(declaration) => fmt::Display::fmt(declaration, f),
+        }
+    }
+}
diff --git a/rust/schema/definable/struct_.rs b/rust/schema/definable/struct_.rs
new file mode 100644
index 000000000..238082181
--- /dev/null
+++ b/rust/schema/definable/struct_.rs
@@ -0,0 +1,51 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{identifier::Identifier, Span},
+    pretty::Pretty,
+    type_::TypeAny,
+};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Struct {
+    span: Option<Span>,
+    ident: Identifier,
+    fields: Vec<Field>,
+}
+
+impl Struct {
+    pub fn new(span: Option<Span>, ident: Identifier, fields: Vec<Field>) -> Self {
+        Self { span, ident, fields }
+    }
+}
+
+impl Pretty for Struct {
+    fn fmt(&self, _indent_level: usize, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+impl fmt::Display for Struct {
+    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        todo!()
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Field {
+    span: Option<Span>,
+    key: Identifier,
+    type_: TypeAny,
+}
+
+impl Field {
+    pub fn new(span: Option<Span>, key: Identifier, type_: TypeAny) -> Self {
+        Self { span, key, type_ }
+    }
+}
diff --git a/rust/schema/definable/type_/capability.rs b/rust/schema/definable/type_/capability.rs
new file mode 100644
index 000000000..ca24f2398
--- /dev/null
+++ b/rust/schema/definable/type_/capability.rs
@@ -0,0 +1,185 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{identifier::Identifier, token, Span, Spanned},
+    pretty::Pretty,
+    type_::{Label, ScopedLabel, Type, TypeAny},
+    util::write_joined,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Alias {
+    span: Option<Span>,
+    aliases: Vec<Label>,
+}
+
+impl Alias {
+    pub fn new(span: Option<Span>, aliases: Vec<Label>) -> Self {
+        Self { span, aliases }
+    }
+}
+
+impl Spanned for Alias {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl fmt::Display for Alias {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} ", token::Keyword::Alias)?;
+        write_joined!(f, ", ", &self.aliases)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Sub {
+    span: Option<Span>,
+    supertype_label: Label,
+}
+
+impl Sub {
+    pub fn new(span: Option<Span>, supertype_label: Label) -> Self {
+        Self { span, supertype_label }
+    }
+
+    pub fn build(supertype_label: impl Into<Identifier>) -> Self {
+        Self::new(None, Label::Identifier(supertype_label.into()))
+    }
+}
+
+impl Spanned for Sub {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl fmt::Display for Sub {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("sub ")?;
+        fmt::Display::fmt(&self.supertype_label, f)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ValueType {
+    span: Option<Span>,
+    value_type: Type,
+}
+
+impl ValueType {
+    pub fn new(span: Option<Span>, value_type: Type) -> Self {
+        Self { span, value_type }
+    }
+}
+
+impl Spanned for ValueType {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl fmt::Display for ValueType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str("value ")?;
+        fmt::Display::fmt(&self.value_type, f)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Owns {
+    span: Option<Span>,
+    owned: TypeAny,
+    overridden: Option<Label>,
+}
+
+impl Owns {
+    pub fn new(span: Option<Span>, owned: TypeAny, overridden: Option<Label>) -> Self {
+        Self { span, owned, overridden }
+    }
+}
+
+impl Spanned for Owns {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Owns {}
+
+impl fmt::Display for Owns {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Owns, self.owned)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Relates {
+    span: Option<Span>,
+    related: TypeAny,
+    overridden: Option<Label>,
+}
+
+impl Relates {
+    pub fn new(span: Option<Span>, related: TypeAny, overridden: Option<Label>) -> Self {
+        Self { span, related, overridden }
+    }
+}
+
+impl Spanned for Relates {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl fmt::Display for Relates {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Relates, self.related)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Plays {
+    span: Option<Span>,
+    role: ScopedLabel,
+    overridden: Option<Label>,
+}
+
+impl Plays {
+    pub fn new(span: Option<Span>, role: ScopedLabel, overridden: Option<Label>) -> Self {
+        Self { span, role, overridden }
+    }
+}
+
+impl Spanned for Plays {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl fmt::Display for Plays {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Plays, self.role)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
diff --git a/rust/schema/definable/type_/mod.rs b/rust/schema/definable/type_/mod.rs
new file mode 100644
index 000000000..0369e1617
--- /dev/null
+++ b/rust/schema/definable/type_/mod.rs
@@ -0,0 +1,141 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use self::capability::{Alias, Owns, Plays, Relates, Sub, ValueType};
+use crate::{
+    annotation::Annotation,
+    common::{Span, Spanned},
+    pretty::{indent, Pretty},
+    type_::{Label, ReservedLabel},
+    util::write_joined,
+};
+
+pub mod capability;
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Type {
+    span: Option<Span>,
+    kind: Option<ReservedLabel>,
+    label: Label,
+    capabilities: Vec<Capability>,
+}
+
+impl Type {
+    pub(crate) fn new(
+        span: Option<Span>,
+        kind: Option<ReservedLabel>,
+        label: Label,
+        capabilities: Vec<Capability>,
+    ) -> Self {
+        Self { span, kind, label, capabilities }
+    }
+
+    pub fn build(label: Label) -> Self {
+        Self::new(None, None, label, Vec::new())
+    }
+}
+
+impl Spanned for Type {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Type {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some(kind) = &self.kind {
+            write!(f, "{} ", kind)?;
+        }
+        write!(f, "{}", self.label)?;
+        if let Some((first, rest)) = self.capabilities.split_first() {
+            write!(f, " {}", first)?;
+            for cap in rest {
+                writeln!(f, ",")?;
+                indent(indent_level, f)?;
+                write!(f, "{}", cap)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Type {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(&self.label, f)?;
+        f.write_char(' ')?;
+        write_joined!(f, ", ", &self.capabilities)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Capability {
+    span: Option<Span>,
+    base: CapabilityBase,
+    annotations: Vec<Annotation>,
+}
+
+impl Capability {
+    pub fn new(span: Option<Span>, base: CapabilityBase, annotations: Vec<Annotation>) -> Self {
+        Self { span, base, annotations }
+    }
+}
+
+impl Spanned for Capability {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Capability {}
+
+impl fmt::Display for Capability {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.base)?;
+        for annotation in &self.annotations {
+            write!(f, " {}", annotation)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum CapabilityBase {
+    Sub(Sub),
+    Alias(Alias),
+    Owns(Owns),
+    Plays(Plays),
+    Relates(Relates),
+    ValueType(ValueType),
+}
+
+impl Spanned for CapabilityBase {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Self::Sub(inner) => inner.span(),
+            Self::Alias(inner) => inner.span(),
+            Self::Owns(inner) => inner.span(),
+            Self::Plays(inner) => inner.span(),
+            Self::Relates(inner) => inner.span(),
+            Self::ValueType(inner) => inner.span(),
+        }
+    }
+}
+
+impl fmt::Display for CapabilityBase {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Sub(inner) => fmt::Display::fmt(inner, f),
+            Self::Alias(inner) => fmt::Display::fmt(inner, f),
+            Self::Owns(inner) => fmt::Display::fmt(inner, f),
+            Self::Plays(inner) => fmt::Display::fmt(inner, f),
+            Self::Relates(inner) => fmt::Display::fmt(inner, f),
+            Self::ValueType(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/pattern/statement/mod.rs b/rust/schema/mod.rs
similarity index 79%
rename from rust/pattern/statement/mod.rs
rename to rust/schema/mod.rs
index 362f954a7..05cf638aa 100644
--- a/rust/pattern/statement/mod.rs
+++ b/rust/schema/mod.rs
@@ -4,6 +4,5 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-pub use self::type_::TypeDeclaration;
-
-pub mod type_;
+pub mod definable;
+pub mod undefinable;
diff --git a/rust/schema/undefinable/mod.rs b/rust/schema/undefinable/mod.rs
new file mode 100644
index 000000000..8cb85cf3b
--- /dev/null
+++ b/rust/schema/undefinable/mod.rs
@@ -0,0 +1,181 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use super::definable::type_::CapabilityBase;
+use crate::{
+    common::{identifier::Identifier, token, Span},
+    pretty::Pretty,
+    type_::Label,
+};
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum Undefinable {
+    Type(Label),                                // undefine person;
+    AnnotationType(AnnotationType),             // undefine @independent from name;
+    AnnotationCapability(AnnotationCapability), // undefine @card from person owns name;
+    CapabilityType(CapabilityType),             // undefine owns name from person;
+    Override(Override),                         // undefine as name from person owns first-name;
+
+    Function(Function), // undefine fun reachable;
+    Struct(Struct),     // undefine struct coords;
+}
+
+impl Pretty for Undefinable {}
+
+impl fmt::Display for Undefinable {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Type(inner) => fmt::Display::fmt(inner, f),
+            Self::AnnotationType(inner) => fmt::Display::fmt(inner, f),
+            Self::AnnotationCapability(inner) => fmt::Display::fmt(inner, f),
+            Self::CapabilityType(inner) => fmt::Display::fmt(inner, f),
+            Self::Override(inner) => fmt::Display::fmt(inner, f),
+            Self::Function(inner) => fmt::Display::fmt(inner, f),
+            Self::Struct(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct AnnotationType {
+    span: Option<Span>,
+    annotation_category: token::Annotation,
+    type_: Label,
+}
+
+impl AnnotationType {
+    pub fn new(span: Option<Span>, annotation_category: token::Annotation, type_: Label) -> Self {
+        Self { span, annotation_category, type_ }
+    }
+}
+
+impl Pretty for AnnotationType {}
+
+impl fmt::Display for AnnotationType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{} {} {}", self.annotation_category, token::Keyword::From, self.type_)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct AnnotationCapability {
+    span: Option<Span>,
+    annotation_category: token::Annotation,
+    type_: Label,
+    capability: CapabilityBase,
+}
+
+impl AnnotationCapability {
+    pub fn new(
+        span: Option<Span>,
+        annotation_category: token::Annotation,
+        type_: Label,
+        capability: CapabilityBase,
+    ) -> Self {
+        Self { span, annotation_category, type_, capability }
+    }
+}
+
+impl Pretty for AnnotationCapability {}
+
+impl fmt::Display for AnnotationCapability {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "@{} {} {} {}", self.annotation_category, token::Keyword::From, self.type_, self.capability)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct CapabilityType {
+    span: Option<Span>,
+    capability: CapabilityBase,
+    type_: Label,
+}
+
+impl CapabilityType {
+    pub fn new(span: Option<Span>, capability: CapabilityBase, type_: Label) -> Self {
+        Self { span, capability, type_ }
+    }
+}
+
+impl Pretty for CapabilityType {}
+
+impl fmt::Display for CapabilityType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {} {}", self.capability, token::Keyword::From, self.type_)
+    }
+}
+
+#[derive(Debug, Eq, PartialEq)]
+pub struct Override {
+    span: Option<Span>,
+    overridden: Label,
+    type_: Label,
+    capability: CapabilityBase,
+}
+
+impl Override {
+    pub fn new(span: Option<Span>, overridden: Label, type_: Label, capability: CapabilityBase) -> Self {
+        Self { span, overridden, type_, capability }
+    }
+}
+
+impl Pretty for Override {}
+
+impl fmt::Display for Override {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{} {} {} {} {}",
+            token::Keyword::As,
+            self.overridden,
+            token::Keyword::From,
+            self.type_,
+            self.capability
+        )
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Function {
+    span: Option<Span>,
+    ident: Identifier,
+}
+
+impl Function {
+    pub fn new(span: Option<Span>, ident: Identifier) -> Self {
+        Self { span, ident }
+    }
+}
+
+impl Pretty for Function {}
+
+impl fmt::Display for Function {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Fun, self.ident)
+    }
+}
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Struct {
+    span: Option<Span>,
+    ident: Identifier,
+}
+
+impl Struct {
+    pub fn new(span: Option<Span>, ident: Identifier) -> Self {
+        Self { span, ident }
+    }
+}
+
+impl Pretty for Struct {}
+
+impl fmt::Display for Struct {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Struct, self.ident)
+    }
+}
diff --git a/rust/statement/comparison.rs b/rust/statement/comparison.rs
new file mode 100644
index 000000000..1e3cf85a9
--- /dev/null
+++ b/rust/statement/comparison.rs
@@ -0,0 +1,55 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{token, Span},
+    expression::Expression,
+    pretty::Pretty,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ComparisonStatement {
+    span: Option<Span>,
+    lhs: Expression,
+    comparison: Comparison,
+}
+
+impl ComparisonStatement {
+    pub fn new(span: Option<Span>, lhs: Expression, comparison: Comparison) -> Self {
+        Self { span, lhs, comparison }
+    }
+}
+
+impl Pretty for ComparisonStatement {}
+
+impl fmt::Display for ComparisonStatement {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", self.lhs, self.comparison)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Comparison {
+    span: Option<Span>,
+    pub comparator: token::Comparator,
+    pub rhs: Expression,
+}
+
+impl Comparison {
+    pub(crate) fn new(span: Option<Span>, comparator: token::Comparator, rhs: Expression) -> Self {
+        Self { span, comparator, rhs }
+    }
+}
+
+impl Pretty for Comparison {}
+
+impl fmt::Display for Comparison {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", self.comparator, self.rhs)
+    }
+}
diff --git a/rust/statement/mod.rs b/rust/statement/mod.rs
new file mode 100644
index 000000000..fdbf71bed
--- /dev/null
+++ b/rust/statement/mod.rs
@@ -0,0 +1,167 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::{collections::HashMap, fmt};
+
+use self::{
+    comparison::ComparisonStatement,
+    thing::{AttributeComparisonStatement, AttributeValueStatement},
+};
+pub use self::{thing::Thing, type_::Type};
+use crate::{
+    common::{token, Span},
+    expression::{Expression, FunctionCall},
+    pretty::Pretty,
+    type_::Label,
+    util::write_joined,
+    variable::Variable,
+};
+
+pub mod comparison;
+pub mod thing;
+pub mod type_;
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Is {
+    span: Option<Span>,
+    lhs: Variable,
+    rhs: Variable,
+}
+
+impl Is {
+    pub(crate) fn new(span: Option<Span>, lhs: Variable, rhs: Variable) -> Self {
+        Self { span, lhs, rhs }
+    }
+}
+
+impl Pretty for Is {}
+
+impl fmt::Display for Is {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {} {}", self.lhs, token::Keyword::Is, self.rhs)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct InStream {
+    span: Option<Span>,
+    lhs: Vec<Variable>,
+    rhs: FunctionCall,
+}
+
+impl InStream {
+    pub(crate) fn new(span: Option<Span>, lhs: Vec<Variable>, rhs: FunctionCall) -> Self {
+        Self { span, lhs, rhs }
+    }
+}
+
+impl Pretty for InStream {}
+
+impl fmt::Display for InStream {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write_joined!(f, ", ", self.lhs)?;
+        write!(f, " {} {}", token::Keyword::In, self.rhs)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum DeconstructField {
+    Variable(Variable),
+    Deconstruct(StructDeconstruct),
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct StructDeconstruct {
+    span: Option<Span>,
+    field_map: HashMap<Label, DeconstructField>,
+}
+
+impl StructDeconstruct {
+    pub fn new(span: Option<Span>, field_map: HashMap<Label, DeconstructField>) -> Self {
+        Self { span, field_map }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum AssignmentPattern {
+    Variables(Vec<Variable>),
+    Deconstruct(StructDeconstruct),
+}
+
+impl Pretty for AssignmentPattern {}
+
+impl fmt::Display for AssignmentPattern {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variables(vars) => write_joined!(f, ", ", vars),
+            Self::Deconstruct(_) => todo!(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Assignment {
+    span: Option<Span>,
+    lhs: AssignmentPattern,
+    rhs: Expression,
+}
+
+impl Assignment {
+    pub fn new(span: Option<Span>, lhs: AssignmentPattern, rhs: Expression) -> Self {
+        Self { span, lhs, rhs }
+    }
+}
+
+impl Pretty for Assignment {}
+
+impl fmt::Display for Assignment {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} = {}", self.lhs, self.rhs)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Statement {
+    Is(Is),
+    InStream(InStream),
+    Comparison(ComparisonStatement),
+    Assignment(Assignment),
+    Thing(Thing),
+    AttributeValue(AttributeValueStatement),
+    AttributeComparison(AttributeComparisonStatement),
+    Type(Type),
+}
+
+impl Pretty for Statement {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Statement::Is(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::InStream(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::Comparison(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::Assignment(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::Thing(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::AttributeValue(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::AttributeComparison(inner) => Pretty::fmt(inner, indent_level, f),
+            Statement::Type(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Statement {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Statement::Is(inner) => fmt::Display::fmt(inner, f),
+            Statement::InStream(inner) => fmt::Display::fmt(inner, f),
+            Statement::Comparison(inner) => fmt::Display::fmt(inner, f),
+            Statement::Assignment(inner) => fmt::Display::fmt(inner, f),
+            Statement::Thing(inner) => fmt::Display::fmt(inner, f),
+            Statement::AttributeValue(inner) => fmt::Display::fmt(inner, f),
+            Statement::AttributeComparison(inner) => fmt::Display::fmt(inner, f),
+            Statement::Type(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
diff --git a/rust/statement/thing/isa.rs b/rust/statement/thing/isa.rs
new file mode 100644
index 000000000..7e76e6bbe
--- /dev/null
+++ b/rust/statement/thing/isa.rs
@@ -0,0 +1,52 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{token, Span},
+    pretty::Pretty,
+    type_::Type,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Isa {
+    span: Option<Span>,
+    pub kind: IsaKind,
+    pub type_: Type,
+}
+
+impl Isa {
+    pub fn new(span: Option<Span>, kind: IsaKind, type_: Type) -> Self {
+        Self { span, kind, type_ }
+    }
+}
+
+impl Pretty for Isa {}
+
+impl fmt::Display for Isa {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", self.kind, self.type_)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum IsaKind {
+    Exact,
+    Subtype,
+}
+
+impl Pretty for IsaKind {}
+
+impl fmt::Display for IsaKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let token = match self {
+            Self::Exact => token::Keyword::IsaX,
+            Self::Subtype => token::Keyword::Isa,
+        };
+        write!(f, "{}", token)
+    }
+}
diff --git a/rust/statement/thing/mod.rs b/rust/statement/thing/mod.rs
new file mode 100644
index 000000000..1e66107a6
--- /dev/null
+++ b/rust/statement/thing/mod.rs
@@ -0,0 +1,304 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use self::isa::Isa;
+use super::{comparison, Statement};
+use crate::{
+    common::{token, Span},
+    expression::Expression,
+    pretty::{indent, Pretty},
+    type_::{Type, TypeAny},
+    util::write_joined,
+    value::Literal,
+    variable::Variable,
+};
+
+pub mod isa;
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Thing {
+    span: Option<Span>,
+    pub head: Head,
+    pub constraints: Vec<Constraint>,
+}
+
+impl Thing {
+    pub fn new(span: Option<Span>, head: Head, constraints: Vec<Constraint>) -> Self {
+        Self { span, head, constraints }
+    }
+}
+
+impl From<Thing> for Statement {
+    fn from(val: Thing) -> Self {
+        Statement::Thing(val)
+    }
+}
+
+impl Pretty for Thing {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.head)?;
+        if let Some((first, rest)) = self.constraints.split_first() {
+            f.write_char(' ')?;
+            Pretty::fmt(first, indent_level, f)?;
+            for constraint in rest {
+                f.write_str(",\n")?;
+                indent(indent_level + 1, f)?;
+                Pretty::fmt(constraint, indent_level + 1, f)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Thing {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.head)?;
+        if let Some((first, rest)) = self.constraints.split_first() {
+            write!(f, " {}", first)?;
+            for constraint in rest {
+                write!(f, ", {}", constraint)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Head {
+    Variable(Variable),
+    Relation(Relation),
+}
+
+impl Pretty for Head {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Relation(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Head {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => fmt::Display::fmt(inner, f),
+            Self::Relation(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Relation {
+    span: Option<Span>,
+    pub role_players: Vec<RolePlayer>,
+}
+
+impl Relation {
+    pub(crate) fn new(span: Option<Span>, role_players: Vec<RolePlayer>) -> Self {
+        Self { span, role_players }
+    }
+}
+
+impl Pretty for Relation {}
+
+impl fmt::Display for Relation {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_char('(')?;
+        write_joined!(f, ", ", self.role_players)?;
+        f.write_char(')')?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum RolePlayer {
+    Typed(TypeAny, Variable),
+    Untyped(Variable),
+}
+
+impl Pretty for RolePlayer {}
+
+impl fmt::Display for RolePlayer {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            RolePlayer::Typed(type_, var) => write!(f, "{type_}: {var}"),
+            RolePlayer::Untyped(var) => write!(f, "{var}"),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct AttributeValueStatement {
+    span: Option<Span>,
+    type_: Option<Type>,
+    value: Literal,
+    isa: Isa,
+}
+
+impl AttributeValueStatement {
+    pub fn new(span: Option<Span>, type_: Option<Type>, value: Literal, isa: Isa) -> Self {
+        Self { span, type_, value, isa }
+    }
+}
+
+impl Pretty for AttributeValueStatement {}
+
+impl fmt::Display for AttributeValueStatement {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some(type_) = &self.type_ {
+            write!(f, "{} ", type_)?;
+        }
+        write!(f, "{} {}", self.value, self.isa)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct AttributeComparisonStatement {
+    span: Option<Span>,
+    var: Variable,
+    comparison: comparison::Comparison,
+    isa: Isa,
+}
+
+impl AttributeComparisonStatement {
+    pub fn new(span: Option<Span>, var: Variable, comparison: comparison::Comparison, isa: Isa) -> Self {
+        Self { span, var, comparison, isa }
+    }
+}
+
+impl Pretty for AttributeComparisonStatement {}
+
+impl fmt::Display for AttributeComparisonStatement {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {} {}", self.var, self.comparison, self.isa)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Constraint {
+    Isa(Isa),
+    Iid(Iid),
+    Has(Has),
+    Links(Links),
+}
+
+impl Pretty for Constraint {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Isa(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Iid(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Has(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Links(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for Constraint {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Isa(inner) => fmt::Display::fmt(inner, f),
+            Self::Iid(inner) => fmt::Display::fmt(inner, f),
+            Self::Has(inner) => fmt::Display::fmt(inner, f),
+            Self::Links(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Iid {
+    span: Option<Span>,
+    iid: String,
+}
+
+impl Iid {
+    pub(crate) fn new(span: Option<Span>, iid: String) -> Self {
+        Self { span, iid }
+    }
+}
+
+impl Pretty for Iid {}
+
+impl fmt::Display for Iid {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::IID, self.iid)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Has {
+    span: Option<Span>,
+    pub type_: Option<TypeAny>,
+    pub value: HasValue,
+}
+
+impl Has {
+    pub fn new(span: Option<Span>, type_: Option<TypeAny>, value: HasValue) -> Self {
+        Self { span, type_, value }
+    }
+}
+
+impl Pretty for Has {}
+
+impl fmt::Display for Has {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", token::Keyword::Has)?;
+        if let Some(type_) = &self.type_ {
+            write!(f, " {}", type_)?;
+        }
+        write!(f, " {}", self.value)?;
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum HasValue {
+    Variable(Variable),
+    Expression(Expression),
+    Comparison(comparison::Comparison),
+}
+
+impl Pretty for HasValue {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Expression(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Comparison(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for HasValue {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Variable(inner) => fmt::Display::fmt(inner, f),
+            Self::Expression(inner) => fmt::Display::fmt(inner, f),
+            Self::Comparison(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Links {
+    span: Option<Span>,
+    pub relation: Relation,
+}
+
+impl Links {
+    pub fn new(span: Option<Span>, relation: Relation) -> Self {
+        Self { span, relation }
+    }
+}
+
+impl Pretty for Links {}
+
+impl fmt::Display for Links {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Links, self.relation)
+    }
+}
diff --git a/rust/statement/type_.rs b/rust/statement/type_.rs
new file mode 100644
index 000000000..5f289559c
--- /dev/null
+++ b/rust/statement/type_.rs
@@ -0,0 +1,268 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt::{self, Write};
+
+use crate::{
+    annotation::Annotation,
+    common::{token, Span},
+    pretty::{indent, Pretty},
+    type_::{Label, ScopedLabel, Type as TypeRef, TypeAny},
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Type {
+    span: Option<Span>,
+    pub type_: TypeRef,
+    pub constraints: Vec<Constraint>,
+}
+
+impl Type {
+    pub fn new(span: Option<Span>, type_: TypeRef, constraints: Vec<Constraint>) -> Self {
+        Self { span, type_, constraints }
+    }
+}
+
+impl Pretty for Type {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.type_)?;
+        if let Some((first, rest)) = self.constraints.split_first() {
+            f.write_char(' ')?;
+            Pretty::fmt(first, indent_level, f)?;
+            for constraint in rest {
+                f.write_str(",\n")?;
+                indent(indent_level + 1, f)?;
+                Pretty::fmt(constraint, indent_level, f)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl fmt::Display for Type {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.type_)?;
+        if let Some((first, rest)) = self.constraints.split_first() {
+            write!(f, " {}", first)?;
+            for constraint in rest {
+                write!(f, ", {}", constraint)?;
+            }
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Constraint {
+    span: Option<Span>,
+    pub base: ConstraintBase,
+    pub annotations: Vec<Annotation>,
+}
+
+impl Constraint {
+    pub fn new(span: Option<Span>, base: ConstraintBase, annotations: Vec<Annotation>) -> Self {
+        Self { span, base, annotations }
+    }
+}
+
+impl Pretty for Constraint {}
+
+impl fmt::Display for Constraint {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.base)?;
+        for annotation in &self.annotations {
+            write!(f, " {}", annotation)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum ConstraintBase {
+    Sub(Sub),
+    Label(LabelConstraint),
+    ValueType(ValueType),
+    Owns(Owns),
+    Relates(Relates),
+    Plays(Plays),
+}
+
+impl Pretty for ConstraintBase {
+    fn fmt(&self, indent_level: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Sub(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Label(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::ValueType(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Owns(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Relates(inner) => Pretty::fmt(inner, indent_level, f),
+            Self::Plays(inner) => Pretty::fmt(inner, indent_level, f),
+        }
+    }
+}
+
+impl fmt::Display for ConstraintBase {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Sub(inner) => fmt::Display::fmt(inner, f),
+            Self::Label(inner) => fmt::Display::fmt(inner, f),
+            Self::ValueType(inner) => fmt::Display::fmt(inner, f),
+            Self::Owns(inner) => fmt::Display::fmt(inner, f),
+            Self::Relates(inner) => fmt::Display::fmt(inner, f),
+            Self::Plays(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum LabelConstraint {
+    Name(Label),
+    Scoped(ScopedLabel),
+}
+
+impl Pretty for LabelConstraint {}
+
+impl fmt::Display for LabelConstraint {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Name(inner) => write!(f, "{} {}", token::Keyword::Label, inner),
+            Self::Scoped(inner) => write!(f, "{} {}", token::Keyword::Label, inner),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum SubKind {
+    Direct,
+    Transitive,
+}
+
+impl Pretty for SubKind {}
+
+impl fmt::Display for SubKind {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let token = match self {
+            Self::Transitive => token::Keyword::Sub,
+            Self::Direct => token::Keyword::SubX,
+        };
+        write!(f, "{}", token)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Sub {
+    span: Option<Span>,
+    kind: SubKind,
+    supertype: TypeRef,
+}
+
+impl Sub {
+    pub fn new(span: Option<Span>, kind: SubKind, supertype: TypeRef) -> Self {
+        Self { span, kind, supertype }
+    }
+}
+
+impl Pretty for Sub {}
+
+impl fmt::Display for Sub {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", self.kind, self.supertype)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ValueType {
+    span: Option<Span>,
+    value_type: TypeRef,
+}
+
+impl ValueType {
+    pub fn new(span: Option<Span>, value_type: TypeRef) -> Self {
+        Self { span, value_type }
+    }
+}
+
+impl Pretty for ValueType {}
+
+impl fmt::Display for ValueType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::ValueType, self.value_type)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Owns {
+    span: Option<Span>,
+    owned: TypeAny,
+    overridden: Option<TypeRef>,
+}
+
+impl Owns {
+    pub fn new(span: Option<Span>, owned: TypeAny, overridden: Option<TypeRef>) -> Self {
+        Self { span, owned, overridden }
+    }
+}
+
+impl Pretty for Owns {}
+
+impl fmt::Display for Owns {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Owns, self.owned)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Relates {
+    span: Option<Span>,
+    related: TypeAny,
+    overridden: Option<TypeRef>,
+}
+
+impl Relates {
+    pub fn new(span: Option<Span>, related: TypeAny, overridden: Option<TypeRef>) -> Self {
+        Self { span, related, overridden }
+    }
+}
+
+impl Pretty for Relates {}
+
+impl fmt::Display for Relates {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Relates, self.related)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Plays {
+    span: Option<Span>,
+    role: TypeRef,
+    overridden: Option<TypeRef>,
+}
+
+impl Plays {
+    pub fn new(span: Option<Span>, role: TypeRef, overridden: Option<TypeRef>) -> Self {
+        Self { span, role, overridden }
+    }
+}
+
+impl Pretty for Plays {}
+
+impl fmt::Display for Plays {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{} {}", token::Keyword::Plays, self.role)?;
+        if let Some(overridden) = &self.overridden {
+            write!(f, " {} {}", token::Keyword::As, overridden)?;
+        }
+        Ok(())
+    }
+}
diff --git a/rust/type_.rs b/rust/type_.rs
new file mode 100644
index 000000000..3466d888e
--- /dev/null
+++ b/rust/type_.rs
@@ -0,0 +1,198 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{identifier::Identifier, token, Span, Spanned},
+    pretty::Pretty,
+    schema,
+    variable::Variable,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct BuiltinValueType {
+    span: Option<Span>,
+    name: token::ValueType,
+}
+
+impl BuiltinValueType {
+    pub fn new(span: Option<Span>, name: token::ValueType) -> Self {
+        Self { span, name }
+    }
+}
+
+impl Pretty for BuiltinValueType {}
+
+impl fmt::Display for BuiltinValueType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}", self.name)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Type {
+    Label(Label),                   // person
+    ScopedLabel(ScopedLabel),       // friendship:friend
+    Variable(Variable),             // $t
+    BuiltinValue(BuiltinValueType), // string
+}
+
+impl Pretty for Type {}
+
+impl fmt::Display for Type {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Label(inner) => fmt::Display::fmt(inner, f),
+            Self::ScopedLabel(inner) => fmt::Display::fmt(inner, f),
+            Self::Variable(inner) => fmt::Display::fmt(inner, f),
+            Self::BuiltinValue(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct ReservedLabel {
+    span: Option<Span>,
+    token: token::Type,
+}
+
+impl ReservedLabel {
+    pub fn new(span: Option<Span>, token: token::Type) -> Self {
+        Self { span, token }
+    }
+}
+
+impl fmt::Display for ReservedLabel {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(&self.token, f)
+    }
+}
+
+impl Spanned for ReservedLabel {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub enum Label {
+    Identifier(Identifier),
+    Reserved(ReservedLabel),
+}
+
+impl Label {
+    pub fn as_str(&self) -> &str {
+        match self {
+            Label::Identifier(ident) => ident.as_str(),
+            Label::Reserved(reserved) => reserved.token.as_str(),
+        }
+    }
+}
+
+impl From<Label> for schema::definable::Type {
+    fn from(label: Label) -> Self {
+        Self::build(label)
+    }
+}
+
+impl Spanned for Label {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Self::Identifier(inner) => inner.span(),
+            Self::Reserved(inner) => inner.span(),
+        }
+    }
+}
+
+impl fmt::Display for Label {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Identifier(inner) => fmt::Display::fmt(inner, f),
+            Self::Reserved(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub struct ScopedLabel {
+    span: Option<Span>,
+    scope: Label,
+    name: Label,
+}
+
+impl ScopedLabel {
+    pub fn new(span: Option<Span>, scope: Label, name: Label) -> Self {
+        Self { span, scope, name }
+    }
+}
+
+impl Pretty for ScopedLabel {}
+
+impl fmt::Display for ScopedLabel {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}:{}", self.scope, self.name)
+    }
+}
+#[derive(Debug, Clone, Eq, PartialEq)]
+// TODO name?
+pub enum TypeAny {
+    Type(Type),         // person, friendship:friend, or $t
+    Optional(Optional), // person?, friendship:friend?, or $t?
+    List(List),         // person[], friendship:friend[], or $t[]
+}
+
+impl Pretty for TypeAny {}
+
+impl fmt::Display for TypeAny {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Self::Type(inner) => fmt::Display::fmt(inner, f),
+            Self::Optional(inner) => fmt::Display::fmt(inner, f),
+            Self::List(inner) => fmt::Display::fmt(inner, f),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Optional {
+    span: Option<Span>,
+    inner: Type,
+}
+
+impl Optional {
+    pub fn new(span: Option<Span>, inner: Type) -> Self {
+        Self { span, inner }
+    }
+}
+
+impl Pretty for Optional {}
+
+impl fmt::Display for Optional {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}?", self.inner)
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct List {
+    span: Option<Span>,
+    inner: Type,
+}
+
+impl List {
+    pub fn new(span: Option<Span>, inner: Type) -> Self {
+        Self { span, inner }
+    }
+}
+
+impl Pretty for List {}
+
+impl fmt::Display for List {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "{}[]", self.inner)
+    }
+}
diff --git a/rust/typeql.rs b/rust/typeql.rs
index b0273c17f..2539dad9e 100644
--- a/rust/typeql.rs
+++ b/rust/typeql.rs
@@ -4,49 +4,53 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+#![deny(rust_2018_idioms)]
+#![deny(rust_2021_compatibility)]
+#![deny(rust_2024_compatibility)]
 #![deny(elided_lifetimes_in_paths)]
 #![deny(unused_must_use)]
 
+pub mod annotation;
 pub mod builder;
 pub mod common;
+pub mod expression;
 pub mod parser;
 pub mod pattern;
+mod pretty;
 pub mod query;
-#[macro_use]
+pub mod schema;
+pub mod statement;
+pub mod type_;
 mod util;
-
-pub use common::Result;
-use parser::visit_eof_query;
-use query::Query;
+mod value;
+mod variable;
+
+use schema::definable::Struct;
+
+use crate::parser::{visit_eof_definition_function, visit_eof_definition_struct, visit_eof_label, visit_eof_query};
+pub use crate::{
+    common::Result,
+    pattern::Pattern,
+    query::Query,
+    schema::definable::{Definable, Function},
+    statement::Statement,
+    type_::{Label, ScopedLabel, Type, TypeAny},
+    value::Literal,
+    variable::Variable,
+};
 
 pub fn parse_query(typeql_query: &str) -> Result<Query> {
     visit_eof_query(typeql_query.trim_end())
 }
 
-// pub fn parse_queries(typeql_queries: &str) -> Result<impl Iterator<Item = Result<Query>> + '_> {
-//     visit_eof_queries(typeql_queries.trim_end())
-// }
-
-// pub fn parse_pattern(typeql_pattern: &str) -> Result<Pattern> {
-//     visit_eof_pattern(typeql_pattern.trim_end())
-// }
-
-// pub fn parse_patterns(typeql_patterns: &str) -> Result<Vec<Pattern>> {
-//     visit_eof_patterns(typeql_patterns.trim_end())
-// }
-
-// pub fn parse_definables(typeql_definables: &str) -> Result<Vec<Definable>> {
-//     visit_eof_definables(typeql_definables.trim_end())
-// }
-
-// pub fn parse_rule(typeql_rule: &str) -> Result<Rule> {
-//     visit_eof_schema_rule(typeql_rule.trim_end())
-// }
+pub fn parse_label(typeql_label: &str) -> Result<Label> {
+    visit_eof_label(typeql_label)
+}
 
-// pub fn parse_statement(typeql_statement: &str) -> Result<Statement> {
-//     visit_eof_statement(typeql_statement.trim_end())
-// }
+pub fn parse_definition_function(typeql_function: &str) -> Result<Function> {
+    visit_eof_definition_function(typeql_function.trim_end())
+}
 
-// pub fn parse_label(typeql_label: &str) -> Result<Label> {
-//     visit_eof_label(typeql_label)
-// }
+pub fn parse_definition_struct(typeql_struct: &str) -> Result<Struct> {
+    visit_eof_definition_struct(typeql_struct.trim_end())
+}
diff --git a/rust/util/mod.rs b/rust/util/mod.rs
index 96cf55cf6..377494223 100644
--- a/rust/util/mod.rs
+++ b/rust/util/mod.rs
@@ -4,7 +4,28 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
-#[macro_export]
+macro_rules! write_joined {
+    ($f:ident, $joiner:expr, $($iterable:expr),* $(,)?) => {{
+        let mut result: std::fmt::Result = Ok(());
+        let mut _is_first = true;
+        let joiner = $joiner;
+        $(
+        let mut iter = $iterable.iter();
+        if result.is_ok() && _is_first {
+            if let Some(head) = iter.next() {
+                _is_first = false;
+                result = std::fmt::Display::fmt(head, $f);
+            }
+        }
+        if result.is_ok() {
+            result = iter.try_for_each(|x| std::fmt::Display::fmt(&joiner, $f).and_then(|()| std::fmt::Display::fmt(x, $f)));
+        }
+        )*
+        result
+    }};
+}
+pub(crate) use write_joined;
+
 macro_rules! enum_getter {
     {$enum_name:ident $($fn_name:ident ( $enum_variant:ident ) => $typename:ty),* $(,)?} => {
         impl $enum_name {
@@ -15,7 +36,8 @@ macro_rules! enum_getter {
                     )*
                 }
             }
-            $(
+        }
+        $(impl $enum_name {
             pub fn $fn_name(self) -> $typename {
                 match self {
                     Self::$enum_variant(x) => x,
@@ -26,40 +48,8 @@ macro_rules! enum_getter {
                         typename: stringify!($typename)
                     }),
                 }
-            })*
-        }
-    };
-}
-
-#[macro_export]
-macro_rules! enum_wrapper {
-    {$enum_name:ident $($typename:ty => $enum_value:ident),* $(,)?} => {
-        $(impl From<$typename> for $enum_name {
-            fn from(x: $typename) -> Self {
-                Self::$enum_value(x)
             }
         })*
     };
 }
-
-#[macro_export]
-macro_rules! write_joined {
-    ($f:ident, $joiner:expr, $($iterable:expr),* $(,)?) => {{
-        let mut result: std::fmt::Result = Ok(());
-        let mut _is_first = true;
-        let joiner = $joiner;
-        $(
-        let mut iter = $iterable.iter();
-        if result.is_ok() && _is_first {
-            if let Some(head) = iter.next() {
-                _is_first = false;
-                result = std::fmt::Display::fmt(head, $f);
-            }
-        }
-        if result.is_ok() {
-            result = iter.try_for_each(|x| std::fmt::Display::fmt(&joiner, $f).and_then(|()| std::fmt::Display::fmt(x, $f)));
-        }
-        )*
-        result
-    }};
-}
+pub(crate) use enum_getter;
diff --git a/rust/value.rs b/rust/value.rs
new file mode 100644
index 000000000..8a5658c85
--- /dev/null
+++ b/rust/value.rs
@@ -0,0 +1,136 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{error::TypeQLError, Span, Spanned},
+    pretty::Pretty,
+    Result,
+};
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum Tag {
+    Boolean,
+    Long,
+    Double,
+    Decimal,
+    Date,
+    DateTime,
+    DateTimeTZ,
+    Duration,
+    String,
+    // larger groupings when ambiguous
+    Integral,   // Long OR Fractional
+    Fractional, // Double OR Decimal
+}
+
+impl Tag {
+    fn name(&self) -> &'static str {
+        match self {
+            Self::Boolean => "Boolean",
+            Self::Long => "Long",
+            Self::Double => "Double",
+            Self::Decimal => "Decimal",
+            Self::Date => "Date",
+            Self::DateTime => "DateTime",
+            Self::DateTimeTZ => "DateTimeTZ",
+            Self::Duration => "Duration",
+            Self::String => "String",
+            Self::Integral => "Integral",
+            Self::Fractional => "Fractional",
+        }
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Literal {
+    span: Option<Span>,
+    pub tag: Option<Tag>,
+    inner: String, // TODO this can be smarter
+}
+
+impl Literal {
+    pub(crate) fn new(span: Option<Span>, category: Option<Tag>, inner: String) -> Self {
+        Self { span, tag: category, inner }
+    }
+
+    pub fn parse_to_string(&self) -> Result<String> {
+        if self.tag.is_some_and(|cat| cat != Tag::String) {
+            Err(TypeQLError::InvalidLiteral { expected_variant: "String", variant: self.tag.unwrap().name() }.into())
+        } else {
+            parse_string(&self.inner)
+        }
+    }
+}
+
+impl Spanned for Literal {
+    fn span(&self) -> Option<Span> {
+        self.span
+    }
+}
+
+impl Pretty for Literal {}
+
+impl fmt::Display for Literal {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(&self.inner)
+    }
+}
+
+fn parse_string(escaped_string: &str) -> Result<String> {
+    let bytes = escaped_string.as_bytes();
+    // it's a bug if these fail; either in the parser or the builder
+    assert_eq!(bytes[0], bytes[bytes.len() - 1]);
+    assert!(matches!(bytes[0], b'\'' | b'"'));
+    let escaped_string = &escaped_string[1..escaped_string.len() - 1];
+
+    let mut buf = String::with_capacity(escaped_string.len());
+
+    let mut rest = escaped_string;
+    while !rest.is_empty() {
+        let (char, escaped_len) = if rest.as_bytes()[0] == b'\\' {
+            let bytes = rest.as_bytes();
+
+            if bytes.len() < 2 {
+                return Err(TypeQLError::InvalidStringEscape {
+                    full_string: escaped_string.to_owned(),
+                    escape: String::from(r"\"),
+                }
+                .into());
+            }
+
+            match bytes[1] {
+                BSP => ('\x08', 2),
+                TAB => ('\x09', 2),
+                LF_ => ('\x0a', 2),
+                FF_ => ('\x0c', 2),
+                CR_ => ('\x0d', 2),
+                c @ (b'"' | b'\'' | b'\\') => (c as char, 2),
+                b'u' => todo!("Unicode escape handling"),
+                _ => {
+                    return Err(TypeQLError::InvalidStringEscape {
+                        full_string: escaped_string.to_owned(),
+                        escape: format!(r"\{}", rest.chars().nth(1).unwrap()),
+                    }
+                    .into())
+                }
+            }
+        } else {
+            let char = rest.chars().next().expect("string is non-empty");
+            (char, char.len_utf8())
+        };
+        buf.push(char);
+        rest = &rest[escaped_len..];
+    }
+    Ok(buf)
+}
+
+const BSP: u8 = b'b';
+const TAB: u8 = b't';
+const LF_: u8 = b'n';
+const FF_: u8 = b'f';
+const CR_: u8 = b'r';
diff --git a/rust/variable.rs b/rust/variable.rs
new file mode 100644
index 000000000..69da60cd9
--- /dev/null
+++ b/rust/variable.rs
@@ -0,0 +1,46 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use std::fmt;
+
+use crate::{
+    common::{identifier::Identifier, Span, Spanned},
+    pretty::Pretty,
+};
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Variable {
+    Anonymous(Option<Span>),
+    Named(Option<Span>, Identifier),
+}
+
+impl Variable {
+    pub fn name(&self) -> Option<&str> {
+        match self {
+            Self::Anonymous(_) => None,
+            Self::Named(_, ident) => Some(ident.as_str()),
+        }
+    }
+}
+
+impl Pretty for Variable {}
+
+impl fmt::Display for Variable {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            Variable::Anonymous(_) => write!(f, "$_"),
+            Variable::Named(_, ident) => write!(f, "${ident}"),
+        }
+    }
+}
+
+impl Spanned for Variable {
+    fn span(&self) -> Option<Span> {
+        match self {
+            Variable::Anonymous(span) | Variable::Named(span, _) => *span,
+        }
+    }
+}


Test patch:
diff --git a/rust/parser/test/aggregate.rs b/rust/parser/test/aggregate.rs
new file mode 100644
index 000000000..873975909
--- /dev/null
+++ b/rust/parser/test/aggregate.rs
@@ -0,0 +1,59 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_parsing_aggregate_std() {
+    let query = r#"match
+$x isa movie;
+std($x);"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(var("x").isa("movie")).std(cvar("x"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_has_variable() {
+    let query = r#"match
+$_ has title "Godfather",
+    has tmdb-vote-count $x;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var(()).has(("title", "Godfather")).has(("tmdb-vote-count", cvar("x"))));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_regex_attribute_type() {
+    let query = r#"match
+$x value string @regex("(fe)male");"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(var("x").regex("(fe)male"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_aggregate_count_query() {
+    let query = r#"match
+($x, $y) isa friendship;
+filter $x, $y;
+count($x);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(rel("x").links("y").isa("friendship")).get_fixed([var("x"), cvar("y")]).count();
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_comparing_count_query_using_typeql_and_rust_typeql_they_are_equivalent() {
+    let query = r#"match
+$x isa movie,
+    has title "Godfather";
+count();"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("movie").has(("title", "Godfather"))).count();
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/autogenerated.rs b/rust/parser/test/autogenerated.rs
index 3a3f20be8..be2a04db4 100644
--- a/rust/parser/test/autogenerated.rs
+++ b/rust/parser/test/autogenerated.rs
@@ -7,21 +7,27 @@
 use std::{
     collections::{HashMap, HashSet},
     hash::{Hash, Hasher},
+    sync::{Mutex, OnceLock},
 };
 
 use itertools::Itertools;
 use proc_macro2::{Delimiter, TokenStream, TokenTree};
 
-use crate::parse_query;
+use crate::{parse_definition_function, parse_definition_struct};
+#[allow(unused)]
+use crate::{
+    parse_label, parse_query,
+    parser::{parse_single, Rule},
+};
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
 enum RepKind {
     Maybe,
     Star,
     Plus,
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
 enum Expansion {
     Sequence(Vec<Expansion>),
     Alternatives(Vec<Expansion>),
@@ -30,6 +36,8 @@ enum Expansion {
     Literal(String),
 }
 
+static EXPANSION_IS_RECURSIVE_CACHE: OnceLock<Mutex<HashMap<Expansion, bool>>> = OnceLock::new();
+
 impl Expansion {
     fn flatten(self) -> Self {
         match self {
@@ -47,6 +55,34 @@ impl Expansion {
             Self::Rule(_) | Self::Literal(_) => self,
         }
     }
+
+    fn is_recursive(&self, rules: &HashMap<String, Expansion>) -> bool {
+        self.is_recursive_impl(Default::default(), rules)
+    }
+
+    fn is_recursive_impl(&self, seen: HashSet<&Expansion>, rules: &HashMap<String, Expansion>) -> bool {
+        if let Some(&cached) = EXPANSION_IS_RECURSIVE_CACHE.get_or_init(Mutex::default).lock().unwrap().get(self) {
+            return cached;
+        }
+
+        if seen.contains(self) {
+            EXPANSION_IS_RECURSIVE_CACHE.get().unwrap().lock().unwrap().insert(self.clone(), true);
+            return true;
+        }
+
+        let seen = &seen | &[self].into();
+        let res = match self {
+            Expansion::Sequence(seq) | Expansion::Alternatives(seq) => seq
+                .iter()
+                .filter(|exp| !matches!(exp, Expansion::Literal(_)))
+                .any(|exp| exp.is_recursive_impl(seen.clone(), rules)),
+            Expansion::Repetition(expansion, _, _) => expansion.is_recursive_impl(seen, rules),
+            Expansion::Rule(rule_name) => rules[rule_name].is_recursive_impl(seen, rules),
+            Expansion::Literal(_) => false,
+        };
+        EXPANSION_IS_RECURSIVE_CACHE.get().unwrap().lock().unwrap().insert(self.clone(), res);
+        res
+    }
 }
 
 #[derive(Debug, Default)]
@@ -61,24 +97,24 @@ impl GrammarTree {
         let mut tree = GrammarTree::default();
         tree.rules.insert(
             "ASCII_DIGIT".into(),
-            Expansion::Alternatives(
-                [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().map(|digit| Expansion::Literal(digit.to_string())).collect(),
-            ),
+            Expansion::Alternatives((0..=9).map(|digit| Expansion::Literal(digit.to_string())).collect()),
         );
         tree.rules.insert(
             "ASCII_HEX_DIGIT".into(),
             Expansion::Alternatives(
-                [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].into_iter().map(|digit| Expansion::Literal(digit.to_string())).collect(),
+                ('0'..='9').chain('a'..='f').map(|digit| Expansion::Literal(digit.to_string())).collect(),
             ),
         );
+        tree.rules.insert(
+            "ASCII_ALPHANUMERIC".into(),
+            Expansion::Alternatives(vec![
+                Expansion::Rule("ASCII_ALPHA".to_owned()),
+                Expansion::Rule("ASCII_DIGIT".to_owned()),
+            ]),
+        );
         tree.rules.insert(
             "ASCII_ALPHA".into(),
-            Expansion::Alternatives(
-                ['a', 'b', 'c', 'd', 'e', 'f', 'z']
-                    .into_iter()
-                    .map(|digit| Expansion::Literal(digit.to_string()))
-                    .collect(),
-            ),
+            Expansion::Alternatives(('a'..='z').map(|digit| Expansion::Literal(digit.to_string())).collect()),
         );
         tree.rules.insert(
             "ANY".into(),
@@ -136,8 +172,13 @@ fn visit(
             TokenTree::Ident(ident) => {
                 if ident == "SOI" {
                     tree.roots.insert(rule_name.to_owned());
-                } else if ident != "EOI" && ident != "WB" {
-                    vec.push(Expansion::Rule(ident.to_string()))
+                    children.next(); // skip '~' following SOI
+                } else if ident == "EOI" || ident == "WB" {
+                    if !is_atomic {
+                        vec.pop(); // preceded by `~`
+                    }
+                } else {
+                    vec.push(Expansion::Rule(ident.to_string()));
                 }
             }
             TokenTree::Group(group) => {
@@ -171,6 +212,7 @@ fn visit(
                         vec.push(Expansion::Literal(" ".into()))
                     }
                 }
+                '^' => (), // ignore
                 '!' => {
                     children.next(); //skip
                 }
@@ -203,29 +245,58 @@ fn visit(
                 }
                 _ => unreachable!("unexpected punctuation"),
             },
-            _ => unreachable!("unexpected token tree"),
         }
     }
     Expansion::Sequence(vec)
 }
 
+fn grammar() -> GrammarTree {
+    GrammarTree::from_grammar(include_str!("../typeql.pest"))
+}
+
+const ITERS_PER_DEPTH: usize = 10;
+const MIN_DEPTH: usize = 3;
+const MAX_DEPTH: usize = 10;
+
 #[test]
-fn test() {
-    let tree = GrammarTree::from_grammar(include_str!("../typeql.pest"));
-    let rules: HashMap<_, _> = tree.rules.into_iter().map(|(name, expansion)| (name, expansion.flatten())).collect();
+fn can_parse_generated() {
+    let tree = grammar();
 
-    for _ in 0..100 {
-        let typeql_query = generate(&rules, &rules["query_define"]);
-        eprintln!("{typeql_query}");
-        parse_query(&typeql_query).unwrap();
+    for _ in 0..ITERS_PER_DEPTH {
+        for max_depth in MIN_DEPTH..=MAX_DEPTH {
+            macro_rules! assert_parses {
+                ($rule:ident) => {
+                    let typeql_query = generate(&tree.rules, stringify!($rule), max_depth);
+                    parse_single(Rule::$rule, &typeql_query).inspect_err(|_| eprintln!("{typeql_query}\n")).unwrap();
+                };
+            }
+            assert_parses!(eof_query);
+        }
     }
+}
+
+#[test]
+fn all_rules_covered_by_visitors() {
+    let tree = grammar();
 
-    // for root in tree.roots {
-    // let rule = &rules[&root];
-    // for _ in 0..100 {
-    // parse(generate(&rules, rule)).unwrap();
-    // }
-    // }
+    let parsers: HashMap<&'static str, fn(&str)> = [
+        ("eof_query", (|s| parse_query(s).map(|_| ()).unwrap()) as fn(&str)),
+        ("eof_label", (|s| parse_label(s).map(|_| ()).unwrap()) as fn(&str)),
+        ("eof_definition_function", (|s| parse_definition_function(s).map(|_| ()).unwrap()) as fn(&str)),
+        ("eof_definition_struct", (|s| parse_definition_struct(s).map(|_| ()).unwrap()) as fn(&str)),
+    ]
+    .into();
+
+    for rule in tree.roots {
+        if !parsers.contains_key(rule.as_str()) {
+            continue; // FIXME should be removed
+        }
+        for _ in 0..ITERS_PER_DEPTH {
+            for max_depth in MIN_DEPTH..=MAX_DEPTH {
+                parsers[rule.as_str()](&generate(&tree.rules, &rule, max_depth));
+            }
+        }
+    }
 }
 
 fn bad_rng() -> u64 {
@@ -234,53 +305,51 @@ fn bad_rng() -> u64 {
     hasher.finish()
 }
 
-const MAX_DEPTH: usize = 31;
 const MAX_REP: u64 = 5;
 
-fn generate(rules: &HashMap<String, Expansion>, rule: &Expansion) -> String {
+fn generate(rules: &HashMap<String, Expansion>, rule_name: &str, max_depth: usize) -> String {
     let space = Expansion::Literal(" ".into());
     let mut buf = String::new();
-    let mut stack = vec![rule];
-    while !stack.is_empty() {
-        let rule = stack.remove(0);
+    let mut stack = vec![&rules[rule_name]];
+    while let Some(rule) = stack.pop() {
         match rule {
-            Expansion::Sequence(seq) => stack = [&seq.iter().collect_vec()[..], &stack[..]].concat(),
+            Expansion::Sequence(seq) => stack.extend(seq.iter().rev()),
             Expansion::Alternatives(alts) => {
-                let index = if stack.len() >= MAX_DEPTH {
-                    0 // first alt should never be recursive, so this is probably a safe heuristic
+                let alt = if alts.len() > 100 || stack.len() < max_depth {
+                    &alts[bad_rng() as usize % alts.len()]
                 } else {
-                    bad_rng() as usize % alts.len()
+                    let finite_alts = alts.iter().filter(|exp| !exp.is_recursive(rules)).collect_vec();
+                    if finite_alts.is_empty() {
+                        &alts[0] // first branch should be more likely to terminate earlier
+                    } else {
+                        finite_alts[bad_rng() as usize % finite_alts.len()]
+                    }
                 };
-                stack.insert(0, &alts[index])
+                stack.push(alt)
             }
-            Expansion::Repetition(rule, RepKind::Maybe, _) => {
-                if stack.len() < MAX_DEPTH && bad_rng() % 2 == 1 {
-                    stack.insert(0, rule)
-                }
-            }
-            Expansion::Repetition(rule, RepKind::Plus, false) => {
-                stack = (0..(bad_rng() % (MAX_REP - 1) + 1))
-                    .map(|_| &**rule)
-                    .intersperse(&space)
-                    .chain(stack.into_iter())
-                    .collect()
-            }
-            Expansion::Repetition(rule, RepKind::Star, false) => {
-                let num = bad_rng() % MAX_REP;
-                if num > 0 && stack.len() < MAX_DEPTH {
-                    stack = (0..num).map(|_| &**rule).intersperse(&space).chain(stack.into_iter()).collect()
-                }
-            }
-            Expansion::Repetition(rule, RepKind::Plus, true) => {
-                stack = (0..(bad_rng() % (MAX_REP - 1) + 1)).map(|_| &**rule).chain(stack.into_iter()).collect()
-            }
-            Expansion::Repetition(rule, RepKind::Star, true) => {
-                let num = bad_rng() % MAX_REP;
-                if num > 0 && stack.len() < MAX_DEPTH {
-                    stack = (0..num).map(|_| &**rule).chain(stack.into_iter()).collect()
+            Expansion::Repetition(rule, rep_kind, is_atomic) => {
+                let num = if stack.len() >= max_depth && rule.is_recursive(rules) {
+                    match rep_kind {
+                        RepKind::Maybe | RepKind::Star => 0,
+                        RepKind::Plus => 1,
+                    }
+                } else {
+                    match rep_kind {
+                        RepKind::Maybe => bad_rng() % 2,
+                        RepKind::Star => bad_rng() % MAX_REP,
+                        RepKind::Plus => bad_rng() % (MAX_REP - 1) + 1,
+                    }
+                };
+                if num > 0 {
+                    let reps = (0..num).map(|_| &**rule);
+                    if *is_atomic {
+                        stack.extend(reps)
+                    } else {
+                        stack.extend(Itertools::intersperse(reps, &space))
+                    }
                 }
             }
-            Expansion::Rule(rule) => stack.insert(0, &rules[rule]),
+            Expansion::Rule(rule) => stack.push(&rules[rule]),
             Expansion::Literal(literal) => buf.push_str(literal),
         }
     }
diff --git a/rust/parser/test/builder.rs b/rust/parser/test/builder.rs
new file mode 100644
index 000000000..172c9b4a5
--- /dev/null
+++ b/rust/parser/test/builder.rs
@@ -0,0 +1,91 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+// #[test]
+// fn when_building_invalid_iid_throw() {
+//     let iid = "invalid";
+//     let expected = typeql_match!(var("x").iid(iid));
+//     assert!(expected.is_err());
+// }
+
+// #[test]
+// fn test_utf8_variable() {
+//     let var = "人";
+//     let expected = typeql_match!(var(var).isa("person"));
+//     assert!(expected.is_ok());
+// }
+
+// #[test]
+// fn when_using_invalid_variable_throw() {
+//     let var = "_人";
+//     let expected = typeql_match!(var(var).isa("person"));
+//     assert!(expected.is_err());
+// }
+
+// #[test]
+// fn test_utf8_label() {
+//     let label = "人";
+//     let expected = typeql_match!(var("x").isa(label));
+//     assert!(expected.is_ok());
+// }
+
+// #[test]
+// fn test_utf8_value() {
+//     let value = "人";
+//     let expected = typeql_match!(var("x").isa("person").has(("name", value)));
+//     assert!(expected.is_ok());
+// }
+
+// #[test]
+// fn test_builder_precedence_operators() {
+//     let query = r#"match
+// $a = ($b + $c) * $d;"#;
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("a").assign(cvar("b").add(cvar("c")).multiply(cvar("d"))));
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_builder_associativity_left() {
+//     let query = r#"match
+// $a = $b - ($c - $d);"#;
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("a").assign(cvar("b").subtract(cvar("c").subtract(cvar("d")))));
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_builder_associativity_right() {
+//     let query = r#"match
+// $a = ($b ^ $c) ^ $d;"#;
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("a").assign(cvar("b").power(cvar("c")).power(cvar("d"))));
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_parenthesis_preserving() {
+//     let query = r#"match
+// $a = $b + ($c + $d) + $e * ($f * $g);"#;
+
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("a")
+//         .assign(var("b").add(cvar("c").add(cvar("d"))).add(cvar("e").multiply(cvar("f").multiply(cvar("g"))))))
+//     ;
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_parenthesis_not_adding_unnecessary() {
+//     let query = r#"match
+// $a = $b + $c + $d + $e * $f * $g;"#;
+
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("a")
+//         .assign(var("b").add(cvar("c")).add(cvar("d")).add(cvar("e").multiply(cvar("f")).multiply(cvar("g")))))
+//     ;
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
diff --git a/rust/parser/test/builtin_functions.rs b/rust/parser/test/builtin_functions.rs
new file mode 100644
index 000000000..57d05669d
--- /dev/null
+++ b/rust/parser/test/builtin_functions.rs
@@ -0,0 +1,116 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_function_min() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$gross = min($net * 1.21, $net + 100.0);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("gross").assign(min!(vvar("net").multiply(1.21), vvar("net").add(100.0)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_function_max() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$gross = max($net * 1.21, $net + 100.0);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("gross").assign(max!(vvar("net").multiply(1.21), vvar("net").add(100.0)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_function_abs() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$value = abs($net * 1.21);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("value").assign(abs(vvar("net").multiply(1.21)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_function_ceil() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$value = ceil($net * 1.21);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("value").assign(ceil(vvar("net").multiply(1.21)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_function_floor() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$value = floor($net * 1.21);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("value").assign(floor(vvar("net").multiply(1.21)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_function_round() {
+    let query = r#"match
+$x isa commodity,
+    has price $p;
+(commodity: $x, qty: $q) isa order;
+$net = $p * $q;
+$value = round($net * 1.21);"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("commodity").has(("price", cvar("p"))),
+    //         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
+    //         var("net").assign(cvar("p").multiply(cvar("q"))),
+    //         var("value").assign(round(vvar("net").multiply(1.21)))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/disjunctions.rs b/rust/parser/test/disjunctions.rs
new file mode 100644
index 000000000..f8fefd644
--- /dev/null
+++ b/rust/parser/test/disjunctions.rs
@@ -0,0 +1,83 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_or_query() {
+    let query = r#"match
+$x isa movie;
+{
+    $y == "drama" isa genre;
+    ($x, $y);
+} or {
+    $x == "The Muppets";
+};"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(
+    //     var("x").isa("movie"),
+    //     or!(and!(var("y").eq("drama").isa("genre"), rel("x").links("y")), cvar("x").eq("The Muppets"))
+    // );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_disjunction_not_in_conjunction() {
+    let query = r#"match
+{
+    $x isa person;
+} or {
+    $x isa company;
+};"#;
+    parse_query(query).unwrap();
+}
+
+#[test]
+fn test_nested_conjunction_and_disjunction() {
+    let query = r#"match
+$y isa $p;
+{
+    ($y, $q);
+} or {
+    $x isa $p;
+    {
+        $x has first-name $y;
+    } or {
+        $x has last-name $z;
+    };
+};"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("y").isa(cvar("p")),
+    //         or!(
+    //             rel("y").links("q"),
+    //             and!(
+    //                 var("x").isa(cvar("p")),
+    //                 or!(var("x").has(("first-name", cvar("y"))), cvar("x").has(("last-name", cvar("z"))))
+    //             )
+    //         )
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_disjunction_not_binding_conjunction() {
+    let query = r#"match
+$y isa $p;
+{
+    ($y, $q);
+} or {
+    $x isa $p;
+    {
+        $x has first-name $y;
+    } or {
+        $q has last-name $z;
+    };
+};"#;
+    let parsed = parse_query(query).unwrap();
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/error.rs b/rust/parser/test/error.rs
new file mode 100644
index 000000000..a238889a1
--- /dev/null
+++ b/rust/parser/test/error.rs
@@ -0,0 +1,119 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_parsing_empty_string() {
+    assert!(parse_query("").is_err());
+}
+#[test]
+fn when_parsing_query_with_syntax_error_report_error() {
+    let query_text = "define\nperson sub entity has name;"; // note no comma
+    let parsed = parse_query(query_text);
+    assert!(parsed.is_err());
+    assert!(parsed.unwrap_err().to_string().contains("person sub entity has name;"));
+}
+
+#[test]
+fn test_missing_colon() {
+    assert!(parse_query("match\n(actor $x, $y) isa has-cast;").is_err());
+}
+
+#[test]
+fn test_missing_comma() {
+    assert!(parse_query("match\n($x $y) isa has-cast;").is_err());
+}
+
+#[test]
+fn test_limit_mistake() {
+    let parsed = parse_query("match\n($x, $y); limit1;");
+    assert!(parsed.is_err());
+    let report = parsed.unwrap_err().to_string();
+    assert!(report.contains("limit1"));
+}
+
+// #[test]
+// fn when_parsing_aggregate_with_wrong_variable_argument_number_throw() {
+//     assert!(parse_query("match\n$x isa name; group;").is_err());
+// }
+
+// #[test]
+// fn when_parsing_aggregate_with_wrong_name_throw() {
+//     assert!(parse_query("match\n$x isa name; hello $x;").is_err());
+// }
+
+// #[test]
+// fn when_value_equality_to_string_create_valid_query_string() {
+//     let expected = typeql_match!(var("x").eq(cvar("y")));
+//     let parsed = parse_query(&expected.to_string()).unwrap();
+//     assert_eq!(expected, parsed);
+// }
+
+#[test]
+fn test_iid_constraint() {
+    let iid = "0x0123456789abcdefdeadbeef";
+    let query = format!(
+        r#"match
+$x iid {iid};"#
+    );
+    let parsed = parse_query(&query).unwrap();
+    // let expected = match_!(var("x").iid(iid));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_invalid_iid_throw() {
+    let query = r#"match
+$x iid invalid;"#;
+    let parsed = parse_query(query);
+    assert!(parsed.is_err());
+}
+
+#[test]
+fn test_parsing_key() {
+    let query = r#"match
+$x owns name @key;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(var("x").owns(("name", Key))).get_fixed([cvar("x")]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_syntax_error_pointer() {
+    let parsed = parse_query("match\n$x of");
+    assert!(parsed.is_err());
+    let report = parsed.unwrap_err().to_string();
+    assert!(report.contains("at 2:4"), "{report}");
+    assert!(report.contains("$x of"), "{report}");
+}
+
+#[test]
+fn when_parsing_incorrect_syntax_trailing_query_whitespace_is_ignored() {
+    let parsed = parse_query("match\n$x isa \n");
+    assert!(parsed.is_err());
+    let report = parsed.unwrap_err().to_string();
+    assert!(report.contains("at 2:7"), "{report}");
+    assert!(report.contains("$x isa"), "{report}");
+}
+
+#[test]
+fn when_parsing_incorrect_syntax_error_message_should_retain_whitespace() {
+    let parsed = parse_query("match\n$x isa ");
+    assert!(parsed.is_err());
+    let report = parsed.unwrap_err().to_string();
+    assert!(!report.contains("match$xisa"), "{report}");
+}
+
+#[test]
+fn when_parsing_incorrect_syntax_throw_typeql_syntax_exception_with_helpful_error() {
+    let parsed = parse_query("match\n$x isa");
+    assert!(parsed.is_err());
+    let report = parsed.unwrap_err().to_string();
+    assert!(report.contains("at 2:7"), "{report}");
+    assert!(report.contains("$x isa"), "{report}");
+}
diff --git a/rust/parser/test/fetch.rs b/rust/parser/test/fetch.rs
new file mode 100644
index 000000000..7c773735d
--- /dev/null
+++ b/rust/parser/test/fetch.rs
@@ -0,0 +1,52 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_fetch_query() {
+    let query = r#"match
+$x isa movie,
+    has title "Godfather",
+    has release-date $d;
+fetch
+$d;
+$d as date;
+$x: name, title as t, name as "Movie name";
+$x as movie: name;
+$x as "Movie name": name;
+label-a: {
+    match
+    ($d, $c) isa director;
+    fetch
+    $d: name, age;
+};
+label-b: {
+    match
+    ($d, $c) isa director;
+    filter $d;
+    count();
+};"#;
+    let parsed = parse_query(query).unwrap();
+    // let projections: Vec<Projection> = vec![
+    // var("d").into(),
+    // var("d").label("date").into(),
+    // var("x").map_attributes(vec!["name".into(), ("title", "t").into(), ("name", "Movie name").into()]),
+    // var("x").label("movie").map_attribute("name"),
+    // var("x").label("Movie name").map_attribute("name"),
+    // label("label-a").map_subquery_fetch(
+    // typeql_match!(rel(var("d")).links(cvar("c")).isa("director"))
+    // .fetch(vec![var("d").map_attributes(vec!["name".into(), "age".into()])]),
+    // ),
+    // label("label-b").map_subquery_get_aggregate(
+    // typeql_match!(rel(var("d")).links(cvar("c")).isa("director")).get_fixed([cvar("d")]).count(),
+    // ),
+    // ];
+    // let expected = typeql_match!(var("x").isa("movie").has(("title", "Godfather")).has(("release-date", cvar("d"))))
+    // .fetch(projections);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/group.rs b/rust/parser/test/group.rs
new file mode 100644
index 000000000..32ddfef4f
--- /dev/null
+++ b/rust/parser/test/group.rs
@@ -0,0 +1,34 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+// #[test]
+// fn test_parsing_aggregate_group() {
+//     let query = r#"match
+// $x isa movie;
+// group $x;"#;
+//     let parsed = parse_query(query).unwrap().into_get_group();
+//     let expected = typeql_match!(var("x").isa("movie")).group(cvar("x"));
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_parsing_aggregate_group_count() {
+//     let query = r#"match
+// $x isa movie;
+// group $x; count;"#;
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let expected = typeql_match!(var("x").isa("movie")).group(cvar("x")).count();
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_parsing_aggregate_to_string() {
+//     let query = r#"match
+// $x isa movie;
+// get $x;
+// group $x; count;"#;
+//     assert_eq!(query, parse_query(query).unwrap().to_string());
+// }
diff --git a/rust/parser/test/group_aggregate.rs b/rust/parser/test/group_aggregate.rs
new file mode 100644
index 000000000..1a1f8ceb1
--- /dev/null
+++ b/rust/parser/test/group_aggregate.rs
@@ -0,0 +1,85 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+// #[test]
+// fn test_aggregate_group_count_query() {
+//     let query = r#"match
+// ($x, $y) isa friendship;
+// get $x, $y;
+// group $x; count;"#;
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let expected =
+//         typeql_match!(rel("x").links("y").isa("friendship")).get_fixed([var("x"), cvar("y")]).group(cvar("x")).count();
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_single_line_group_aggregate_max_query() {
+//     let query = r#"match
+// $x has age $a;
+// group $x; max $a;"#;
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let expected = typeql_match!(var("x").has(("age", cvar("a")))).group(cvar("x")).max(cvar("a"));
+
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_multi_line_group_aggregate_max_query() {
+//     let query = r#"match
+// ($x, $y) isa friendship;
+// $y has age $z;
+// group $x; max $z;"#;
+
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let expected = typeql_match!(rel("x").links("y").isa("friendship"), var("y").has(("age", cvar("z"))))
+//
+//         .group(var("x"))
+//         .max(var("z"));
+
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_multi_line_filtered_group_aggregate_max_query() {
+//     let query = r#"match
+// ($x, $y) isa friendship;
+// $y has age $z;
+// get $x, $y, $z;
+// group $x; max $z;"#;
+
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let expected = typeql_match!(rel("x").links("y").isa("friendship"), var("y").has(("age", cvar("z"))))
+//         .get_fixed([var("x"), cvar("y"), cvar("z")])
+//         .group(var("x"))
+//         .max(var("z"));
+
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
+
+// #[test]
+// fn test_filtered_group_aggregates_on_value_variable() {
+//     let query = r#"match
+// $i ($x, $s) isa income-source;
+// $i has value $v,
+//     has tax-rate $r;
+// $t = $r * $v;
+// get $x, $t;
+// group $x; sum $t;"#;
+
+//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
+//     let filter: [Variable; 2] = [var("x").into(), vvar("t").into()];
+//     let expected = typeql_match!(
+//         var("i").links("x").links("s").isa("income-source"),
+//         var("i").has(("value", cvar("v"))).has(("tax-rate", cvar("r"))),
+//         var("t").assign(cvar("r").multiply(cvar("v"))),
+//     )
+//     .get_fixed(filter)
+//     .group(var("x"))
+//     .sum(var("t"));
+
+//     assert_valid_eq_repr!(expected, parsed, query);
+// }
diff --git a/rust/parser/test/list.rs b/rust/parser/test/list.rs
new file mode 100644
index 000000000..345a27803
--- /dev/null
+++ b/rust/parser/test/list.rs
@@ -0,0 +1,57 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+// #[test]
+// fn test_parsing_list_one_match() {
+// let queries = "match $y isa movie;";
+// let parsed = parse_queries(queries).unwrap().collect_vec();
+// let expected = vec![typeql_match!(var("y").isa("movie"))];
+// assert_eq!(parsed, expected);
+// }
+
+// #[test]
+// fn test_parsing_list_one_insert() {
+// let queries = "insert $x isa movie;";
+// let parsed = parse_queries(queries).unwrap().collect_vec();
+// let expected = vec![typeql_insert!(var("x").isa("movie"))];
+// assert_eq!(parsed, expected);
+// }
+
+// #[test]
+// fn test_parsing_list_one_insert_with_whitespace_prefix() {
+// let queries = " insert $x isa movie;";
+// let parsed = parse_queries(queries).unwrap().collect_vec();
+// let expected = vec![typeql_insert!(var("x").isa("movie"))];
+// assert_eq!(parsed, expected);
+// }
+
+// #[test]
+// fn test_parsing_list_one_insert_with_prefix_comment() {
+// let queries = r#"#hola
+// insert $x isa movie;"#;
+// let parsed = parse_queries(queries).unwrap().collect_vec();
+// let expected = vec![typeql_insert!(var("x").isa("movie"))];
+// assert_eq!(parsed, expected);
+// }
+
+// #[test]
+// fn test_parsing_list() {
+// let queries = "insert $x isa movie; match $y isa movie;";
+// let parsed = parse_queries(queries).unwrap().collect_vec();
+// let expected = vec![typeql_insert!(var("x").isa("movie")).into(), typeql_match!(cvar("y").isa("movie")).into()];
+// assert_eq!(parsed, expected);
+// }
+
+// #[test]
+// fn test_parsing_many_match_insert_without_stack_overflow() {
+//     let num_queries = 10_000;
+//     let query = "match $x isa person; insert $x has name 'bob';";
+//     let queries = query.repeat(num_queries);
+//     let mut parsed = Vec::with_capacity(num_queries);
+//     parsed.extend(parse_queries(&queries).unwrap());
+//     let expected = typeql_match!(var("x").isa("person")).insert(cvar("x").has(("name", "bob")));
+//     assert_eq!(vec![expected; num_queries], parsed);
+// }
diff --git a/rust/parser/test/match_queries.rs b/rust/parser/test/match_queries.rs
new file mode 100644
index 000000000..21c9521c5
--- /dev/null
+++ b/rust/parser/test/match_queries.rs
@@ -0,0 +1,479 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_simple_query() {
+    let query = r#"match
+$x isa movie;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").isa("movie"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_named_type_variable() {
+    let query = r#"match
+$a label attribute_label;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("a").type_("attribute_label")).get_fixed([var("a")]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_string_with_slash() {
+    let query = r#"match
+$x isa person,
+    has name "alice/bob";"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = (); //typeql_match!(var("x").isa("person").has(("name", "alice/bob")));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_relation_query() {
+    let query = r#"match
+$brando "Marl B" isa name;
+(actor: $brando, $char, production-with-cast: $prod);
+filter $char, $prod;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(
+    // var("brando").equals("Marl B").isa("name"),
+    // rel(("actor", "brando")).links("char").links(("production-with-cast", "prod")),
+    // )
+    // .get_fixed([var("char"), var("prod")]);
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_role_type_scoped_globally() {
+    let query = r#"match
+$m relates spouse;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("m").relates("spouse"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_role_type_not_scoped() {
+    let query = r#"match
+marriage relates $s;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(type_("marriage").relates(var("s")));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_predicate_query_1() {
+    let query = r#"match
+$x isa movie,
+    has title $t;
+{
+    $t == "Apocalypse Now";
+} or {
+    $t < "Juno";
+    $t > "Godfather";
+} or {
+    $t == "Spy";
+};
+$t != "Apocalypse Now";"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = ();
+    // typeql_match!(
+    // var("x").isa("movie").has(("title", var("t"))),
+    // or!(
+    // var("t").equals("Apocalypse Now"),
+    // and!(var("t").lt("Juno"), var("t").gt("Godfather")),
+    // var("t").equals("Spy"),
+    // ),
+    // var("t").neq("Apocalypse Now"),
+    // )
+    // ;
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_predicate_query_2() {
+    let query = r#"match
+$x isa movie,
+    has title $t;
+{
+    $t <= "Juno";
+    $t >= "Godfather";
+    $t != "Heat";
+} or {
+    $t == "The Muppets";
+};"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(
+    // var("x").isa("movie").has(("title", var("t"))),
+    // or!(and!(var("t").lte("Juno"), var("t").gte("Godfather"), var("t").neq("Heat")), var("t").equals("The Muppets"),),
+    // );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_predicate_query_3() {
+    let query = r#"match
+($x, $y);
+$y isa person,
+    has name $n;
+{
+    $n contains "ar";
+} or {
+    $n like "^M.*$";
+};"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(
+    // rel("x").links("y"),
+    // var("y").isa("person").has(("name", var("n"))),
+    // or!(var("n").contains("ar"), var("n").like("^M.*$")),
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_predicate_query_4() {
+    let query = r#"match
+$x has age $y;
+$y >= $z;
+$z 18 isa age;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = ();
+    // typeql_match!(var("x").has(("age", var("y"))), var("y").gte(var("z")), var("z").equals(18).isa("age"),);
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_concept_variable() {
+    let query = r#"match
+$x sub $z;
+$y sub $z;
+$a isa $x;
+$b isa $y;
+not { $x is $y; };
+not { $a is $b; };"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(
+    // var("x").sub(var("z")),
+    // var("y").sub(var("z")),
+    // var("a").isa(var("x")),
+    // var("b").isa(var("y")),
+    // not(var("x").is(var("y"))),
+    // not(var("a").is(var("b"))),
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_value_equals_variable_query() {
+    let query = r#"match
+$s1 == $s2;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("s1").equals(var("s2")));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+// // TODO: Remove when we fully deprecate '=' for ThingVariable equality.
+// #[test]
+// fn test_value_equals_variable_query_backwards_compatibility() {
+//     let query = r#"match
+// $s1 = $s2;
+// get;"#;
+//
+//     let parsed = parse_query(query).unwrap();
+//     let expected = typeql_match!(var("s1").equals(var("s2")));
+//
+//     assert_eq!(expected, parsed);
+//     assert_eq!(expected, parse_query(parsed.to_string().as_str()).unwrap());
+// }
+
+#[test]
+fn test_movies_released_after_or_at_the_same_time_as_spy() {
+    let query = r#"match
+$x has release-date >= $r;
+$_ has title "Spy",
+    has release-date $r;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(
+    // var("x").has(("release-date", gte(var("r")))),
+    // var(()).has(("title", "Spy")).has(("release-date", var("r"))),
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_predicate() {
+    let query = r#"match
+$x has release-date < 1986-03-03T00:00,
+    has tmdb-vote-count 100,
+    has tmdb-vote-average <= 9.0;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x")
+    // .has((
+    // "release-date",
+    // lt(NaiveDateTime::new(
+    // NaiveDate::from_ymd_opt(1986, 3, 3).unwrap(),
+    // NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
+    // )),
+    // ))
+    // .has(("tmdb-vote-count", 100))
+    // .has(("tmdb-vote-average", lte(9.0))),);
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_time() {
+    let query = r#"match
+$x has release-date 1000-11-12T13:14:15;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(
+    // NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
+    // NaiveTime::from_hms_opt(13, 14, 15).unwrap(),
+    // ),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_big_years() {
+    let query = r#"match
+$x has release-date +12345-12-25T00:00;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(NaiveDate::from_ymd_opt(12345, 12, 25).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_small_years() {
+    let query = r#"match
+$x has release-date 0867-01-01T00:00;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(NaiveDate::from_ymd_opt(867, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_negative_years() {
+    let query = r#"match
+$x has release-date -3200-01-01T00:00;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(NaiveDate::from_ymd_opt(-3200, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_millis() {
+    let query = r#"match
+$x has release-date 1000-11-12T13:14:15.123;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(
+    // NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
+    // NaiveTime::from_hms_milli_opt(13, 14, 15, 123).unwrap(),
+    // ),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_handle_millis_shorthand() {
+    let query = r#"match
+$x has release-date 1000-11-12T13:14:15.1;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").has((
+    // "release-date",
+    // NaiveDateTime::new(
+    // NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
+    // NaiveTime::from_hms_milli_opt(13, 14, 15, 100).unwrap(),
+    // ),
+    // )));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_error_when_parsing_overly_precise_decimal_seconds() {
+    let query = r#"match
+$x has release-date 1000-11-12T13:14:15.0001234567;"#;
+
+    let parsed = parse_query(query);
+    assert!(parsed.is_err());
+    assert!(parsed.unwrap_err().to_string().contains(" at 2:50"));
+}
+
+#[test]
+fn test_parsing_long_predicate_query() {
+    let query = r#"match
+$x isa movie,
+    has tmdb-vote-count <= 400;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").isa("movie").has(("tmdb-vote-count", lte(400))));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_attribute_query_by_value_variable() {
+    let query = r#"match
+$x = 5;
+$a == $x isa age;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").assign(5), var("a").equals(var("x")).isa("age"));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_precedence_operators() {
+    let query = r#"match
+$res = $a / $b * $c + $d ^ $e ^ $f / $g;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("res").assign(
+    // var("a").divide(var("b")).multiply(var("c")).add(var("d").power(var("e").power(var("f"))).divide(var("g"))),
+    // ));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_precedence_function_and_parentheses() {
+    let query = r#"match
+$res = $a + (round($b + $c) + $d) * $e;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected =
+    // match_!(var("res").assign(var("a").add(round(var("b").add(var("c"))).add(var("d")).multiply(var("e")))));
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_schema_query() {
+    let query = r#"match
+$x plays starring:actor;
+sort $x asc;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").plays(("starring", "actor"))).sort([(cvar("x"), Asc)]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_as_in_match_result_is_same_as_sub() {
+    let query = r#"match
+$f sub parenthood,
+    relates father as parent,
+    relates son as child;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected =
+    //         typeql_match!(var("f").sub("parenthood").relates(("father", "parent")).relates(("son", "child")));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_match_value_type_query() {
+    let query = r#"match
+$x value double;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").value(ValueType::Double));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_date_keyword_parse_as_the_correct_value_type() {
+    let query = r#"match
+$x value datetime;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").value(ValueType::DateTime));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_boolean() {
+    let query = r#"insert
+$_ has flag true;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_insert!(var(()).has(("flag", true)));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_variables_everywhere_query() {
+    let query = r#"match
+($p: $x, $y);
+$x isa $z;
+$y == "crime";
+$z sub production;
+has-genre relates $p;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(
+    // rel((var("p"), cvar("x"))).links("y"),
+    // var("x").isa(cvar("z")),
+    // var("y").eq("crime"),
+    // var("z").sub("production"),
+    // type_("has-genre").relates(var("p")),
+    // );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_parsing_relates_type_variable() {
+    let query = r#"match
+$x isa $type;
+$type relates someRole;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(var("x").isa(cvar("type")), cvar("type").relates("someRole"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/mod.rs b/rust/parser/test/mod.rs
index 5e3f1f7ee..61511d59f 100644
--- a/rust/parser/test/mod.rs
+++ b/rust/parser/test/mod.rs
@@ -4,20 +4,36 @@
  * file, You can obtain one at https://mozilla.org/MPL/2.0/.
  */
 
+mod aggregate;
 mod autogenerated;
-
-use crate::{parse_query, typeql_define};
+mod builder;
+mod builtin_functions;
+mod disjunctions;
+mod error;
+mod fetch;
+mod group;
+mod group_aggregate;
+mod list;
+mod match_queries;
+mod modifiers;
+mod nonquery;
+mod regex;
+mod schema_queries;
+mod write_queries;
+
+use crate::parse_query;
 
 macro_rules! assert_valid_eq_repr {
     ($expected:ident, $parsed:ident, $query:ident) => {
         let parsed = $parsed;
-        let expected = $expected;
+        // let expected = $expected;
         let query = $query;
-        // assert_eq!(expected, parsed);
-        // assert_eq!(format!("{:#}", expected), query);
+        // assert_eq!(format!("{:#}", expected), query, "\n{expected:#}\n\n{query}\n");
         assert_eq!(format!("{:#}", parsed), query, "\n{parsed:#}\n\n{query}\n");
+        // assert_eq!(parsed, expected.into());
     };
 }
+use assert_valid_eq_repr;
 
 // #[test]
 fn tmp() {
@@ -38,7 +54,7 @@ fn tmp() {
     //   match
     //    $x isa person, has name $name;
     //   return {$x};
-    
+
     // fun test_stream_many($x: person) -> {name, age, dob}:
     //   match
     //     $x isa person, has name $name, has age $age, has dob $dob;
@@ -47,24 +63,24 @@ fn tmp() {
     //   offset 10;
     //   limit 10;
     //   return { $name, $age, $dob };
-    
+
     // fun test_single_1($x: person) -> long:
     //   match
     //     $x isa person;
     //   return count($x);
-    
+
     // fun test_single_many($x: person) -> long, long:
     //   match
     //     $x isa person, has age $a;
     //   return count($x), sum($a);
-    
+
     // fun test_stream_optional($x: person) -> { name, age?, dob }:
     //   match
     //     $x isa person, has name $name;
     //     try { $x has age $age; };
     //     $y in get_all_dob($x);
     //   return { $x, $age, $y };
-    
+
     // fun test_single_optional($x: person) -> name?, long, double?:
     //   match
     //     $x isa person, has age $age;
@@ -73,32 +89,32 @@ fn tmp() {
     //  "#;
     // let result = TypeQLParser::parse(Rule::eof_query, query_fn);
     // dbg!("{}", &result);
-    
+
     // let query_list_card = r#"
     // match
     // $x is $y;
-    
+
     // $x > $y;
     // $x like "abc";
     // $x like $y;
     // $x == $y;
-    
+
     // $x = $y;
     // $x = 10 + 11;
-    
+
     // person sub attribute @abstract,
     //   value long @values(1,2,3);
-    
+
     // $person sub attribute @abstract;
     // $person sub $parent, value string @regex("abc");
-    
+
     // $person type person;
-    
+
     // $x sub entity,
     //     owns age as abstract_age @card(0,*) @key @unique,
     //     owns name[] @card(0, *) @distinct,
     //     owns $attr[];
-    
+
     // $x sub relation,
     //     relates friend @card(10, 100) @cascade,
     //     relates best-friend[] @distinct,
@@ -109,1847 +125,53 @@ fn tmp() {
     // dbg!("{}", &result);
 }
 
-// #[test]
-// fn test_simple_query() {
-//     let query = r#"match
-// $x isa movie;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("movie")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_named_type_variable() {
-//     let query = r#"match
-// $a type attribute_label;
-// get $a;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("a").type_("attribute_label")).get_fixed([cvar("a")]);
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_string_with_slash() {
-//     let query = r#"match
-// $x isa person,
-//     has name "alice/bob";
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("person").has(("name", "alice/bob"))).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_relation_query() {
-//     let query = r#"match
-// $brando "Marl B" isa name;
-// (actor: $brando, $char, production-with-cast: $prod);
-// get $char, $prod;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("brando").eq("Marl B").isa("name"),
-//         rel(("actor", "brando")).links("char").links(("production-with-cast", "prod")),
-//     )
-//         .get_fixed([cvar("char"), cvar("prod")]);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_role_type_scoped_globally() {
-//     let query = r#"match
-// $m relates spouse;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("m").relates("spouse")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_role_type_not_scoped() {
-//     let query = r#"match
-// marriage relates $s;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(type_("marriage").relates(cvar("s"))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_predicate_query_1() {
-//     let query = r#"match
-// $x isa movie,
-//     has title $t;
-// {
-//     $t "Apocalypse Now";
-// } or {
-//     $t < "Juno";
-//     $t > "Godfather";
-// } or {
-//     $t "Spy";
-// };
-// $t != "Apocalypse Now";
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected =
-//         typeql_match!(
-//             cvar("x").isa("movie").has(("title", cvar("t"))),
-//             or!(
-//                 cvar("t").eq("Apocalypse Now"),
-//                 and!(cvar("t").lt("Juno"), cvar("t").gt("Godfather")),
-//                 cvar("t").eq("Spy"),
-//             ),
-//             cvar("t").neq("Apocalypse Now"),
-//         )
-//             .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_predicate_query_2() {
-//     let query = r#"match
-// $x isa movie,
-//     has title $t;
-// {
-//     $t <= "Juno";
-//     $t >= "Godfather";
-//     $t != "Heat";
-// } or {
-//     $t "The Muppets";
-// };
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("movie").has(("title", cvar("t"))),
-//         or!(
-//             and!(cvar("t").lte("Juno"), cvar("t").gte("Godfather"), cvar("t").neq("Heat")),
-//             cvar("t").eq("The Muppets"),
-//         ),
-//     )
-//         .get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_predicate_query_3() {
-//     let query = r#"match
-// ($x, $y);
-// $y isa person,
-//     has name $n;
-// {
-//     $n contains "ar";
-// } or {
-//     $n like "^M.*$";
-// };
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         rel("x").links("y"),
-//         cvar("y").isa("person").has(("name", cvar("n"))),
-//         or!(cvar("n").contains("ar"), cvar("n").like("^M.*$")),
-//     )
-//         .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_predicate_query_4() {
-//     let query = r#"match
-// $x has age $y;
-// $y >= $z;
-// $z 18 isa age;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected =
-//         typeql_match!(cvar("x").has(("age", cvar("y"))), cvar("y").gte(cvar("z")), cvar("z").eq(18).isa("age"),).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_concept_variable() {
-//     let query = r#"match
-// $x sub $z;
-// $y sub $z;
-// $a isa $x;
-// $b isa $y;
-// not { $x is $y; };
-// not { $a is $b; };
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").sub(cvar("z")),
-//         cvar("y").sub(cvar("z")),
-//         cvar("a").isa(cvar("x")),
-//         cvar("b").isa(cvar("y")),
-//         not(cvar("x").is(cvar("y"))),
-//         not(cvar("a").is(cvar("b"))),
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_value_equals_variable_query() {
-//     let query = r#"match
-// $s1 == $s2;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("s1").eq(cvar("s2"))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// // TODO: Remove when we fully deprecate '=' for ThingVariable equality.
-// #[test]
-// fn test_value_equals_variable_query_backwards_compatibility() {
-//     let query = r#"match
-// $s1 = $s2;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("s1").eq(cvar("s2"))).get();
-
-//     assert_eq!(expected, parsed);
-//     assert_eq!(expected, parse_query(parsed.to_string().as_str()).unwrap().into_get());
-// }
-
-// #[test]
-// fn test_movies_released_after_or_at_the_same_time_as_spy() {
-//     let query = r#"match
-// $x has release-date >= $r;
-// $_ has title "Spy",
-//     has release-date $r;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").has(("release-date", gte(cvar("r")))),
-//         cvar(()).has(("title", "Spy")).has(("release-date", cvar("r"))),
-//     )
-//     .get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_predicate() {
-//     let query = r#"match
-// $x has release-date < 1986-03-03T00:00,
-//     has tmdb-vote-count 100,
-//     has tmdb-vote-average <= 9.0;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x")
-//         .has((
-//             "release-date",
-//             lt(NaiveDateTime::new(
-//                 NaiveDate::from_ymd_opt(1986, 3, 3).unwrap(),
-//                 NaiveTime::from_hms_opt(0, 0, 0).unwrap()
-//             ))
-//         ))
-//         .has(("tmdb-vote-count", 100))
-//         .has(("tmdb-vote-average", lte(9.0))))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_time() {
-//     let query = r#"match
-// $x has release-date 1000-11-12T13:14:15;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(
-//             NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
-//             NaiveTime::from_hms_opt(13, 14, 15).unwrap()
-//         ),
-//     )))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_big_years() {
-//     let query = r#"match
-// $x has release-date +12345-12-25T00:00;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(NaiveDate::from_ymd_opt(12345, 12, 25).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
-//     )))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_small_years() {
-//     let query = r#"match
-// $x has release-date 0867-01-01T00:00;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(NaiveDate::from_ymd_opt(867, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
-//     )))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_negative_years() {
-//     let query = r#"match
-// $x has release-date -3200-01-01T00:00;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(NaiveDate::from_ymd_opt(-3200, 1, 1).unwrap(), NaiveTime::from_hms_opt(0, 0, 0).unwrap()),
-//     )))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_millis() {
-//     let query = r#"match
-// $x has release-date 1000-11-12T13:14:15.123;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(
-//             NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
-//             NaiveTime::from_hms_milli_opt(13, 14, 15, 123).unwrap(),
-//         ),
-//     )))
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_date_handle_millis_shorthand() {
-//     let query = r#"match
-// $x has release-date 1000-11-12T13:14:15.1;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").has((
-//         "release-date",
-//         NaiveDateTime::new(
-//             NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
-//             NaiveTime::from_hms_milli_opt(13, 14, 15, 100).unwrap(),
-//         ),
-//     )))
-//     .get();
-
-//     let parsed_query = r#"match
-// $x has release-date 1000-11-12T13:14:15.100;
-// get;"#;
-//     assert_valid_eq_repr!(expected, parsed, parsed_query);
-// }
-
-// #[test]
-// fn when_parsing_date_error_when_parsing_overly_precise_decimal_seconds() {
-//     let query = r#"match
-// $x has release-date 1000-11-12T13:14:15.000123456;
-// get;"#;
-
-//     let parsed = parse_query(query);
-//     assert!(parsed.is_err());
-//     assert!(parsed.unwrap_err().to_string().contains("line 2"));
-// }
-
 // #[test]
 // fn when_parsing_date_error_when_handling_overly_precise_nanos() {
-//     let validated = typeql_match!(cvar("x").has((
+//     let validated = typeql_match!(var("x").has((
 //         "release-date",
 //         NaiveDateTime::new(
 //             NaiveDate::from_ymd_opt(1000, 11, 12).unwrap(),
 //             NaiveTime::from_hms_nano_opt(13, 14, 15, 123450000).unwrap(),
 //         ),
 //     )))
-//     .validated();
+//     ;
 //     assert!(validated.is_err());
 //     assert!(validated.unwrap_err().to_string().contains("more precise than 1 millisecond"));
 // }
 
 // #[test]
-// fn test_parsing_long_predicate_query() {
-//     let query = r#"match
-// $x isa movie,
-//     has tmdb-vote-count <= 400;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("tmdb-vote-count", lte(400)))).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_attribute_query_by_value_variable() {
+// fn test_parsing_without_var_concept() {
 //     let query = r#"match
-// ?x = 5;
-// $a == ?x isa age;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("x").assign(5), cvar("a").eq(vvar("x")).isa("age"),).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_variable_name_clash_throws() {
-//     let query = r"match\n
-// $z isa person, has age $y;
-// ?y = $y;
-// get;";
-
+// $_ isa person;"#;
 //     let parsed = parse_query(query);
 //     assert!(parsed.is_err());
+//     let built = typeql_match!(var(()).isa("person"));
+//     assert!(built.is_err());
 // }
 
-// #[test]
-// fn test_parsing_precedence_operators() {
-//     let query = r#"match
-// ?res = $a / $b * $c + $d ^ $e ^ $f / $g;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("res").assign(
-//         cvar("a")
-//             .divide(cvar("b"))
-//             .multiply(cvar("c"))
-//             .add(cvar("d").power(cvar("e").power(cvar("f"))).divide(cvar("g")))
-//     ))
-//     .get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_precedence_function_and_parenthesis() {
-//     let query = r#"match
-// ?res = $a + (round($b + ?c) + $d) * ?e;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         vvar("res").assign(cvar("a").add(round(cvar("b").add(vvar("c"))).add(cvar("d")).multiply(vvar("e"))))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_builder_precedence_operators() {
-//     let query = r#"match
-// ?a = ($b + $c) * $d;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("a").assign(cvar("b").add(cvar("c")).multiply(cvar("d")))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_builder_associativity_left() {
-//     let query = r#"match
-// ?a = $b - ($c - $d);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("a").assign(cvar("b").subtract(cvar("c").subtract(cvar("d"))))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_builder_associativity_right() {
-//     let query = r#"match
-// ?a = ($b ^ $c) ^ $d;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("a").assign(cvar("b").power(cvar("c")).power(cvar("d")))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parenthesis_preserving() {
-//     let query = r#"match
-// ?a = $b + ($c + $d) + $e * ($f * $g);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("a")
-//         .assign(cvar("b").add(cvar("c").add(cvar("d"))).add(cvar("e").multiply(cvar("f").multiply(cvar("g"))))))
-//     .get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parenthesis_not_adding_unnecessary() {
-//     let query = r#"match
-// ?a = $b + $c + $d + $e * $f * $g;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(vvar("a")
-//         .assign(cvar("b").add(cvar("c")).add(cvar("d")).add(cvar("e").multiply(cvar("f")).multiply(cvar("g")))))
-//     .get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_min() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?gross = min(?net * 1.21, ?net + 100.0);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("gross").assign(min!(vvar("net").multiply(1.21), vvar("net").add(100.0)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_max() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?gross = max(?net * 1.21, ?net + 100.0);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("gross").assign(max!(vvar("net").multiply(1.21), vvar("net").add(100.0)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_abs() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?value = abs(?net * 1.21);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("value").assign(abs(vvar("net").multiply(1.21)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_ceil() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?value = ceil(?net * 1.21);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("value").assign(ceil(vvar("net").multiply(1.21)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_floor() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?value = floor(?net * 1.21);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("value").assign(floor(vvar("net").multiply(1.21)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_function_round() {
-//     let query = r#"match
-// $x isa commodity,
-//     has price $p;
-// (commodity: $x, qty: $q) isa order;
-// ?net = $p * $q;
-// ?value = round(?net * 1.21);
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("commodity").has(("price", cvar("p"))),
-//         rel(("commodity", "x")).links(("qty", "q")).isa("order"),
-//         vvar("net").assign(cvar("p").multiply(cvar("q"))),
-//         vvar("value").assign(round(vvar("net").multiply(1.21)))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_schema_query() {
-//     let query = r#"match
-// $x plays starring:actor;
-// get;
-// sort $x asc;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").plays(("starring", "actor"))).get().sort([(cvar("x"), Asc)]);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_sort_on_concept_variable() {
-//     let query = r#"match
-// $x isa movie,
-//     has rating $r;
-// get;
-// sort $r desc;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("rating", cvar("r")))).get().sort([(cvar("r"), Desc)]);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_sort_on_value_variable() {
-//     let query = r#"match
-// $x isa movie,
-//     has rating $r;
-// ?l = 100 - $r;
-// get;
-// sort ?l desc;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("movie").has(("rating", cvar("r"))),
-//         vvar("l").assign(constant(100).subtract(cvar("r")))
-//     )
-//     .get()
-//     .sort([(vvar("l"), Desc)]);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_sort_multiple() {
-//     let query = r#"match
-// $x isa movie,
-//     has title $t,
-//     has rating $r;
-// ?rate = $r * 100;
-// get;
-// sort ?rate desc, $t;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("movie").has(("title", cvar("t"))).has(("rating", cvar("r"))),
-//         vvar("rate").assign(cvar("r").multiply(100)),
-//     )
-//     .get()
-//     .sort(sort_vars!((vvar("rate"), Desc), cvar("t")));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_sort_limit() {
-//     let query = r#"match
-// $x isa movie,
-//     has rating $r;
-// get;
-// sort $r; limit 10;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("rating", cvar("r")))).get().sort([cvar("r")]).limit(10);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_sort_offset_limit() {
-//     let query = r#"match
-// $x isa movie,
-//     has rating $r;
-// get;
-// sort $r desc; offset 10; limit 10;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("rating", cvar("r"))))
-//         .get()
-//         .sort([(cvar("r"), Desc)])
-//         .offset(10)
-//         .limit(10);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_get_offset_limit() {
-//     let query = r#"match
-// $y isa movie,
-//     has title $n;
-// get;
-// offset 2; limit 4;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("y").isa("movie").has(("title", cvar("n")))).get().offset(2).limit(4);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_fetch_query() {
-//     let query = r#"match
-// $x isa movie,
-//     has title "Godfather",
-//     has release-date $d;
-// fetch
-// $d;
-// $d as date;
-// $x: name, title as t, name as "Movie name";
-// $x as movie: name;
-// $x as "Movie name": name;
-// label-a: {
-//     match
-//     ($d, $c) isa director;
-//     fetch
-//     $d: name, age;
-// };
-// label-b: {
-//     match
-//     ($d, $c) isa director;
-//     get $d;
-//     count;
-// };"#;
-//     let result = parse_query(query);
-//     let parsed = match result {
-//         Ok(query) => query.into_fetch(),
-//         Err(error) => {
-//             println!("{}", error);
-//             panic!();
-//         }
-//     };
-
-//     let projections: Vec<Projection> = vec![
-//         cvar("d").into(),
-//         cvar("d").label("date").into(),
-//         cvar("x").map_attributes(vec!["name".into(), ("title", "t").into(), ("name", "Movie name").into()]),
-//         cvar("x").label("movie").map_attribute("name"),
-//         cvar("x").label("Movie name").map_attribute("name"),
-//         label("label-a").map_subquery_fetch(
-//             typeql_match!(rel(cvar("d")).links(cvar("c")).isa("director"))
-//                 .fetch(vec![cvar("d").map_attributes(vec!["name".into(), "age".into()])]),
-//         ),
-//         label("label-b").map_subquery_get_aggregate(
-//             typeql_match!(rel(cvar("d")).links(cvar("c")).isa("director")).get_fixed([cvar("d")]).count(),
-//         ),
-//     ];
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("title", "Godfather")).has(("release-date", cvar("d"))))
-//         .fetch(projections);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_variables_everywhere_query() {
-//     let query = r#"match
-// ($p: $x, $y);
-// $x isa $z;
-// $y "crime";
-// $z sub production;
-// has-genre relates $p;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         rel((cvar("p"), cvar("x"))).links("y"),
-//         cvar("x").isa(cvar("z")),
-//         cvar("y").eq("crime"),
-//         cvar("z").sub("production"),
-//         type_("has-genre").relates(cvar("p")),
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_relates_type_variable() {
-//     let query = r#"match
-// $x isa $type;
-// $type relates someRole;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").isa(cvar("type")), cvar("type").relates("someRole")).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_or_query() {
-//     let query = r#"match
-// $x isa movie;
-// {
-//     $y "drama" isa genre;
-//     ($x, $y);
-// } or {
-//     $x "The Muppets";
-// };
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("x").isa("movie"),
-//         or!(and!(cvar("y").eq("drama").isa("genre"), rel("x").links("y")), cvar("x").eq("The Muppets"))
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_disjunction_not_in_conjunction() {
-//     let query = r#"match
-// {
-//     $x isa person;
-// } or {
-//     $x isa company;
-// };
-// get;"#;
-//     let res = parse_query(query);
-//     assert!(res.is_err(), "{res:?}")
-// }
-
-// #[test]
-// fn test_nested_conjunction_and_disjunction() {
-//     let query = r#"match
-// $y isa $p;
-// {
-//     ($y, $q);
-// } or {
-//     $x isa $p;
-//     {
-//         $x has first-name $y;
-//     } or {
-//         $x has last-name $z;
-//     };
-// };
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(
-//         cvar("y").isa(cvar("p")),
-//         or!(
-//             rel("y").links("q"),
-//             and!(
-//                 cvar("x").isa(cvar("p")),
-//                 or!(cvar("x").has(("first-name", cvar("y"))), cvar("x").has(("last-name", cvar("z"))))
-//             )
-//         )
-//     )
-//     .get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_disjunction_not_binding_conjunction() {
-//     let query = r#"match
-// $y isa $p;
-// { ($y, $q); } or { $x isa $p; { $x has first-name $y; } or { $q has last-name $z; }; };
-// get;"#;
-//     let parsed = parse_query(query);
-//     assert!(parsed.is_err());
-// }
-
-// #[test]
-// fn test_aggregate_count_query() {
-//     let query = r#"match
-// ($x, $y) isa friendship;
-// get $x, $y;
-// count;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_aggregate();
-//     let expected = typeql_match!(rel("x").links("y").isa("friendship")).get_fixed([cvar("x"), cvar("y")]).count();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_aggregate_group_count_query() {
-//     let query = r#"match
-// ($x, $y) isa friendship;
-// get $x, $y;
-// group $x; count;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let expected =
-//         typeql_match!(rel("x").links("y").isa("friendship")).get_fixed([cvar("x"), cvar("y")]).group(cvar("x")).count();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_single_line_group_aggregate_max_query() {
-//     let query = r#"match
-// $x has age $a;
-// get;
-// group $x; max $a;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let expected = typeql_match!(cvar("x").has(("age", cvar("a")))).get().group(cvar("x")).max(cvar("a"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_multi_line_group_aggregate_max_query() {
-//     let query = r#"match
-// ($x, $y) isa friendship;
-// $y has age $z;
-// get;
-// group $x; max $z;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let expected = typeql_match!(rel("x").links("y").isa("friendship"), cvar("y").has(("age", cvar("z"))))
-//         .get()
-//         .group(cvar("x"))
-//         .max(cvar("z"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_multi_line_filtered_group_aggregate_max_query() {
-//     let query = r#"match
-// ($x, $y) isa friendship;
-// $y has age $z;
-// get $x, $y, $z;
-// group $x; max $z;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let expected = typeql_match!(rel("x").links("y").isa("friendship"), cvar("y").has(("age", cvar("z"))))
-//         .get_fixed([cvar("x"), cvar("y"), cvar("z")])
-//         .group(cvar("x"))
-//         .max(cvar("z"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_filtered_group_aggregates_on_value_variable() {
-//     let query = r#"match
-// $i ($x, $s) isa income-source;
-// $i has value $v,
-//     has tax-rate $r;
-// ?t = $r * $v;
-// get $x, ?t;
-// group $x; sum ?t;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let filter: [Variable; 2] = [cvar("x").into(), vvar("t").into()];
-//     let expected = typeql_match!(
-//         cvar("i").links("x").links("s").isa("income-source"),
-//         cvar("i").has(("value", cvar("v"))).has(("tax-rate", cvar("r"))),
-//         vvar("t").assign(cvar("r").multiply(cvar("v"))),
-//     )
-//     .get_fixed(filter)
-//     .group(cvar("x"))
-//     .sum(vvar("t"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_comparing_count_query_using_typeql_and_rust_typeql_they_are_equivalent() {
-//     let query = r#"match
-// $x isa movie,
-//     has title "Godfather";
-// get;
-// count;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_aggregate();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("title", "Godfather"))).get().count();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_delete_query() {
-//     let query = r#"match
-// $x isa movie,
-//     has title "The Title";
-// $y isa movie;
-// delete
-// $x isa movie;
-// $y isa movie;"#;
-
-//     let parsed = parse_query(query).unwrap().into_delete();
-//     let expected = typeql_match!(cvar("x").isa("movie").has(("title", "The Title")), cvar("y").isa("movie"))
-//         .delete([cvar("x").isa("movie"), cvar("y").isa("movie")]);
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_insert_query() {
-//     let query = r#"insert
-// $x isa pokemon,
-//     has name "Pichu";
-// $y isa pokemon,
-//     has name "Pikachu";
-// $z isa pokemon,
-//     has name "Raichu";
-// (evolves-from: $x, evolves-to: $y) isa evolution;
-// (evolves-from: $y, evolves-to: $z) isa evolution;"#;
-
-//     let parsed = parse_query(query).unwrap().into_insert();
-//     let expected = typeql_insert!(
-//         cvar("x").isa("pokemon").has(("name", "Pichu")),
-//         cvar("y").isa("pokemon").has(("name", "Pikachu")),
-//         cvar("z").isa("pokemon").has(("name", "Raichu")),
-//         rel(("evolves-from", "x")).links(("evolves-to", "y")).isa("evolution"),
-//         rel(("evolves-from", "y")).links(("evolves-to", "z")).isa("evolution")
-//     );
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_update_query() {
-//     let query = r#"match
-// $x isa person,
-//     has name "alice",
-//     has age $a;
-// delete
-// $x has $a;
-// insert
-// $x has age 25;"#;
-
-//     let parsed = parse_query(query).unwrap().into_update();
-//     let expected = typeql_match!(cvar("x").isa("person").has(("name", "alice")).has(("age", cvar("a"))))
-//         .delete(cvar("x").has(cvar("a")))
-//         .insert(cvar("x").has(("age", 25)));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_as_in_define_result_is_same_as_sub() {
-//     let query = r#"define
-// parent sub role;
-// child sub role;
-// parenthood sub relation,
-//     relates parent,
-//     relates child;
-// fatherhood sub parenthood,
-//     relates father as parent,
-//     relates son as child;"#;
-
-//     let parsed = parse_query(query).unwrap().into_schema().into_define();
-//     let expected = typeql_define!(
-//         type_("parent").sub("role"),
-//         type_("child").sub("role"),
-//         type_("parenthood").sub("relation").relates("parent").relates("child"),
-//         type_("fatherhood").sub("parenthood").relates(("father", "parent")).relates(("son", "child"))
-//     );
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_as_in_match_result_is_same_as_sub() {
-//     let query = r#"match
-// $f sub parenthood,
-//     relates father as parent,
-//     relates son as child;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected =
-//         typeql_match!(cvar("f").sub("parenthood").relates(("father", "parent")).relates(("son", "child"))).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-#[test]
-fn test_define_query_with_owns_overrides() {
-    let query = r#"define
-triangle sub entity;
-triangle owns side-length;
-triangle-right-angled sub triangle;
-triangle-right-angled owns hypotenuse-length as side-length;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(
-//         type_("triangle").sub("entity"),
-//         type_("triangle").owns("side-length"),
-//         type_("triangle-right-angled").sub("triangle"),
-//         type_("triangle-right-angled").owns(("hypotenuse-length", "side-length"))
-    );
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
-
-#[test]
-fn test_define_query_with_relates_overrides() {
-    let query = r#"define
-pokemon sub entity;
-evolves sub relation;
-evolves relates from,
-    relates to;
-evolves-final sub evolves;
-evolves-final relates from-final as from;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(
-//         type_("pokemon").sub("entity"),
-//         type_("evolves").sub("relation"),
-//         type_("evolves").relates("from").relates("to"),
-//         type_("evolves-final").sub("evolves"),
-//         type_("evolves-final").relates(("from-final", "from"))
-    );
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
-
-#[test]
-fn test_define_query_with_plays_overrides() {
-    let query = r#"define
-pokemon sub entity;
-evolves sub relation;
-evolves relates from,
-    relates to;
-evolves-final sub evolves;
-evolves-final relates from-final as from;
-pokemon plays evolves-final:from-final as from;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(
-        // type_("pokemon").sub("entity"),
-        // type_("evolves").sub("relation"),
-        // type_("evolves").relates("from").relates("to"),
-        // type_("evolves-final").sub("evolves"),
-        // type_("evolves-final").relates(("from-final", "from")),
-        // type_("pokemon").plays(("evolves-final", "from-final", "from"))
-    );
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
+#[test]
+fn test_escape_string() {
+    let input = r#"This has \"double quotes\" and a single-quoted backslash: '\\'"#;
+    let query = format!(
+        r#"insert
+$_ isa movie,
+    has title "{input}";"#
+    );
+    let parsed = parse_query(&query).unwrap();
+    //     let expected = typeql_insert!(var(()).isa("movie").has(("title", input)));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
 
 #[test]
-fn test_define_query() {
-    let query = r#"define
-pokemon sub entity;
-evolution sub relation;
-evolves-from sub role;
-evolves-to sub role;
-evolves relates from,
-    relates to;
-pokemon plays evolves:from,
-    plays evolves:to,
-    owns name;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(
-        // type_("pokemon").sub("entity"),
-        // type_("evolution").sub("relation"),
-        // type_("evolves-from").sub("role"),
-        // type_("evolves-to").sub("role"),
-        // type_("evolves").relates("from").relates("to"),
-        // type_("pokemon").plays(("evolves", "from")).plays(("evolves", "to")).owns("name")
-    );
-
-    assert_valid_eq_repr!(expected, parsed, query);
+fn when_parsing_query_with_comments_they_are_ignored() {
+    let query = r#"match
+# there's a comment here
+$x isa###WOW HERES ANOTHER###
+movie; count();"#;
+    let uncommented = r#"match
+$x isa movie;
+count();"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = typeql_match!(var("x").isa("movie")).count();
+    assert_valid_eq_repr!(expected, parsed, uncommented);
 }
-
-// #[test]
-// fn when_parsing_undefine_query_result_is_same_as_java_typeql() {
-//     let query = r#"undefine
-// pokemon sub entity;
-// evolution sub relation;
-// evolves-from sub role;
-// evolves-to sub role;
-// evolves relates from,
-//     relates to;
-// pokemon plays evolves:from,
-//     plays evolves:to,
-//     owns name;"#;
-
-//     let parsed = parse_query(query).unwrap().into_schema().into_undefine();
-//     let expected = typeql_undefine!(
-//         type_("pokemon").sub("entity"),
-//         type_("evolution").sub("relation"),
-//         type_("evolves-from").sub("role"),
-//         type_("evolves-to").sub("role"),
-//         type_("evolves").relates("from").relates("to"),
-//         type_("pokemon").plays(("evolves", "from")).plays(("evolves", "to")).owns("name")
-//     );
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_undefine_rule() {
-//     let query = r#"undefine
-// rule r;"#;
-
-//     let parsed = parse_query(query).unwrap().into_schema().into_undefine();
-//     let expected = typeql_undefine!(rule("r"));
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_match_insert_query() {
-//     let query = r#"match
-// $x isa language;
-// insert
-// $x has name "HELLO";"#;
-
-//     let parsed = parse_query(query).unwrap().into_insert();
-//     let expected = typeql_match!(cvar("x").isa("language")).insert(cvar("x").has(("name", "HELLO")));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-#[test]
-fn test_define_abstract_entity_query() {
-    let query = r#"define
-concrete-type sub entity;
-abstract-type sub entity @abstract;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected =
-        typeql_define!(/*type_("concrete-type").sub("entity"), type_("abstract-type").sub("entity").abstract_()*/);
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
-
-// #[test]
-// fn test_match_value_type_query() {
-//     let query = r#"match
-// $x value double;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").value(ValueType::Double)).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_without_var_concept() {
-//     let query = r#"match
-// $_ isa person;
-// get;"#;
-
-//     let parsed = parse_query(query);
-//     assert!(parsed.is_err());
-//     let built = typeql_match!(cvar(()).isa("person")).get().validated();
-//     assert!(built.is_err());
-// }
-
-// #[test]
-// fn when_parsing_date_keyword_parse_as_the_correct_value_type() {
-//     let query = r#"match
-// $x value datetime;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").value(ValueType::DateTime)).get();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-#[test]
-fn test_define_value_type_query() {
-    let query = r#"define
-my-type sub attribute,
-    value long;"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(/*type_("my-type").sub("attribute").value(ValueType::Long)*/);
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
-
-// #[test]
-// fn test_escape_string() {
-//     let input = r#"This has \"double quotes\" and a single-quoted backslash: '\\'"#;
-
-//     let query = format!(
-//         r#"insert
-// $_ isa movie,
-//     has title "{input}";"#
-//     );
-
-//     let parsed = parse_query(&query).unwrap().into_insert();
-//     let expected = typeql_insert!(cvar(()).isa("movie").has(("title", input)));
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_query_with_comments_they_are_ignored() {
-//     let query = "match\n\n# there's a comment here\n$x isa###WOW HERES ANOTHER###\r\nmovie;
-// get; count;";
-//     let uncommented = r#"match
-// $x isa movie;
-// get;
-// count;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_aggregate();
-//     let expected = typeql_match!(cvar("x").isa("movie")).get().count();
-
-//     assert_valid_eq_repr!(expected, parsed, uncommented);
-// }
-
-// #[test]
-// fn test_parsing_pattern() {
-//     let pattern = r#"{
-//     (wife: $a, husband: $b) isa marriage;
-//     $a has gender "male";
-//     $b has gender "female";
-// }"#;
-
-//     let parsed = parse_pattern(pattern).unwrap().into_conjunction();
-//     let expected = and!(
-//         rel(("wife", "a")).links(("husband", "b")).isa("marriage"),
-//         cvar("a").has(("gender", "male")),
-//         cvar("b").has(("gender", "female"))
-//     );
-
-//     assert_valid_eq_repr!(expected, parsed, pattern);
-// }
-
-// #[test]
-// fn test_parsing_patterns() {
-//     let patterns = r#"(wife: $a, husband: $b) isa marriage;
-//     $a has gender "male";
-//     $b has gender "female";
-// "#;
-
-//     let parsed = parse_patterns(patterns).unwrap().into_iter().map(|p| p.into_statement()).collect::<Vec<_>>();
-//     let expected: Vec<Statement> = vec![
-//         Statement::Thing(rel(("wife", "a")).links(("husband", "b")).isa("marriage")),
-//         Statement::Thing(cvar("a").has(("gender", "male"))),
-//         Statement::Thing(cvar("b").has(("gender", "female"))),
-//     ];
-
-//     assert_eq!(expected, parsed);
-// }
-
-// #[test]
-// fn test_parsing_definables() {
-//     let query = r#"athlete sub person;
-//       runner sub athlete;
-//       sprinter sub runner;"#;
-
-//     let parsed = parse_definables(query).unwrap().into_iter().map(|p| p.into_type_statement()).collect::<Vec<_>>();
-//     let expected =
-//         vec![type_("athlete").sub("person"), type_("runner").sub("athlete"), type_("sprinter").sub("runner")];
-
-//     assert_eq!(expected, parsed);
-// }
-
-// #[test]
-// fn test_parsing_variable_rel() {
-//     let variable = "(wife: $a, husband: $b) isa marriage";
-
-//     let parsed = parse_statement(variable).unwrap();
-//     if let Statement::Thing(parsed_var) = parsed {
-//         let expected = rel(("wife", "a")).links(("husband", "b")).isa("marriage");
-//         assert_valid_eq_repr!(expected, parsed_var, variable);
-//     } else {
-//         panic!("Expected ThingVariable, found {variable:?}.");
-//     }
-// }
-
-// #[test]
-// fn test_parsing_variable_has() {
-//     let variable = "$x has is_interesting true";
-
-//     let parsed = parse_statement(variable).unwrap();
-//     if let Statement::Thing(parsed_var) = parsed {
-//         let expected = cvar("x").has(("is_interesting", true));
-//         assert_valid_eq_repr!(expected, parsed_var, variable);
-//     } else {
-//         panic!("Expected ThingVariable, found {variable:?}.");
-//     }
-// }
-
-// #[test]
-// fn test_parsing_label() {
-//     let label = "label_with-symbols";
-
-//     let parsed = parse_label(label).unwrap();
-//     let expected = Label { scope: None, name: String::from(label) };
-//     assert_eq!(expected, parsed);
-// }
-
-// #[test]
-// fn test_parsing_boolean() {
-//     let query = r#"insert
-// $_ has flag true;"#;
-
-//     let parsed = parse_query(query).unwrap().into_insert();
-//     let expected = typeql_insert!(cvar(()).has(("flag", true)));
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_aggregate_group() {
-//     let query = r#"match
-// $x isa movie;
-// get;
-// group $x;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group();
-//     let expected = typeql_match!(cvar("x").isa("movie")).get().group(cvar("x"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_aggregate_group_count() {
-//     let query = r#"match
-// $x isa movie;
-// get;
-// group $x; count;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_group_aggregate();
-//     let expected = typeql_match!(cvar("x").isa("movie")).get().group(cvar("x")).count();
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_aggregate_std() {
-//     let query = r#"match
-// $x isa movie;
-// get;
-// std $x;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get_aggregate();
-//     let expected = typeql_match!(cvar("x").isa("movie")).get().std(cvar("x"));
-
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_aggregate_to_string() {
-//     let query = r#"match
-// $x isa movie;
-// get $x;
-// group $x; count;"#;
-
-//     assert_eq!(query, parse_query(query).unwrap().to_string());
-// }
-
-// #[test]
-// fn when_parsing_incorrect_syntax_throw_typeql_syntax_exception_with_helpful_error() {
-//     let parsed = parse_query("match\n$x isa");
-//     assert!(parsed.is_err());
-//     let report = parsed.unwrap_err().to_string();
-//     assert!(report.contains("line 2"));
-//     assert!(report.contains("$x isa"));
-// }
-
-// #[test]
-// fn when_parsing_incorrect_syntax_trailing_query_whitespace_is_ignored() {
-//     let parsed = parse_query("match\n$x isa \n");
-//     assert!(parsed.is_err());
-//     let report = parsed.unwrap_err().to_string();
-//     assert!(report.contains("line 2"));
-//     assert!(report.contains("$x isa"));
-// }
-
-// #[test]
-// fn when_parsing_incorrect_syntax_error_message_should_retain_whitespace() {
-//     let parsed = parse_query("match\n$x isa ");
-//     assert!(parsed.is_err());
-//     let report = parsed.unwrap_err().to_string();
-//     assert!(!report.contains("match$xisa"));
-// }
-
-// #[test]
-// fn test_syntax_error_pointer() {
-//     let parsed = parse_query("match\n$x of");
-//     assert!(parsed.is_err());
-//     let report = parsed.unwrap_err().to_string();
-//     assert!(report.contains("line 2"));
-//     assert!(report.contains("$x of"));
-// }
-
-// #[test]
-// fn test_has_variable() {
-//     let query = r#"match
-// $_ has title "Godfather",
-//     has tmdb-vote-count $x;
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar(()).has(("title", "Godfather")).has(("tmdb-vote-count", cvar("x")))).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_regex_attribute_type() {
-//     let query = r#"match
-// $x regex "(fe)male";
-// get;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").regex("(fe)male")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_typeql_parsing_query() {
-//     assert!(matches!(parse_query("match\n$x isa movie; get;"), Ok(Query::Get(_))));
-// }
-
-// #[test]
-// fn test_parsing_key() {
-//     let query = r#"match
-// $x owns name @key;
-// get $x;"#;
-
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").owns(("name", Key))).get_fixed([cvar("x")]);
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn test_parsing_empty_string() {
-//     assert!(parse_query("").is_err());
-// }
-
-// #[test]
-// fn test_parsing_list_one_match() {
-//     let queries = "match $y isa movie;
-// get;";
-//     let parsed = parse_queries(queries).unwrap().map(|q| q.unwrap().into_get()).collect::<Vec<_>>();
-//     let expected = vec![typeql_match!(cvar("y").isa("movie")).get()];
-//     assert_eq!(parsed, expected);
-// }
-
-// #[test]
-// fn test_parsing_list_one_insert() {
-//     let queries = "insert $x isa movie;";
-//     let parsed = parse_queries(queries).unwrap().map(|q| q.unwrap().into_insert()).collect::<Vec<_>>();
-//     let expected = vec![typeql_insert!(cvar("x").isa("movie"))];
-//     assert_eq!(parsed, expected);
-// }
-
-// #[test]
-// fn test_parsing_list_one_insert_with_whitespace_prefix() {
-//     let queries = " insert $x isa movie;";
-//     let parsed = parse_queries(queries).unwrap().map(|q| q.unwrap().into_insert()).collect::<Vec<_>>();
-//     let expected = vec![typeql_insert!(cvar("x").isa("movie"))];
-//     assert_eq!(parsed, expected);
-// }
-
-// #[test]
-// fn test_parsing_list_one_insert_with_prefix_comment() {
-//     let queries = r#"#hola
-// insert $x isa movie;"#;
-//     let parsed = parse_queries(queries).unwrap().map(|q| q.unwrap().into_insert()).collect::<Vec<_>>();
-//     let expected = vec![typeql_insert!(cvar("x").isa("movie"))];
-//     assert_eq!(parsed, expected);
-// }
-
-// #[test]
-// fn test_parsing_list() {
-//     let queries = "insert $x isa movie; match $y isa movie; get;";
-//     let parsed = parse_queries(queries).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
-//     let expected =
-//         vec![typeql_insert!(cvar("x").isa("movie")).into(), typeql_match!(cvar("y").isa("movie")).get().into()];
-//     assert_eq!(parsed, expected);
-// }
-
-// #[test]
-// fn test_parsing_many_match_insert_without_stack_overflow() {
-//     let num_queries = 10_000;
-//     let query = "match\n$x isa person; insert $x has name 'bob';";
-//     let queries = query.repeat(num_queries);
-
-//     let mut parsed = Vec::with_capacity(num_queries);
-//     parsed.extend(parse_queries(&queries).unwrap().map(|q| q.unwrap().into_insert()));
-
-//     let expected = typeql_match!(cvar("x").isa("person")).insert(cvar("x").has(("name", "bob")));
-
-//     assert_eq!(vec![expected; num_queries], parsed);
-// }
-
-// #[test]
-// fn when_parsing_list_of_queries_with_syntax_error_report_error() {
-//     let query_text = "define\nperson sub entity has name;"; // note no comma
-//     let parsed = parse_query(query_text);
-//     assert!(parsed.is_err());
-//     assert!(parsed.unwrap_err().to_string().contains("person sub entity has name;"));
-// }
-
-// #[test]
-// fn when_parsing_multiple_queries_like_one_throw() {
-//     assert!(parse_query("insert\n$x isa movie; insert $y isa movie").is_err());
-// }
-
-// #[test]
-// fn test_missing_colon() {
-//     assert!(parse_query("match\n(actor $x, $y) isa has-cast;").is_err());
-// }
-
-// #[test]
-// fn test_missing_comma() {
-//     assert!(parse_query("match\n($x $y) isa has-cast;").is_err());
-// }
-
-// #[test]
-// fn test_limit_mistake() {
-//     let parsed = parse_query("match\n($x, $y); limit1;");
-//     assert!(parsed.is_err());
-//     let report = parsed.unwrap_err().to_string();
-//     assert!(report.contains("limit1"));
-// }
-
-// #[test]
-// fn when_parsing_aggregate_with_wrong_variable_argument_number_throw() {
-//     assert!(parse_query(
-//         "match\n$x isa name;
-// get; group;"
-//     )
-//     .is_err());
-// }
-
-// #[test]
-// fn when_parsing_aggregate_with_wrong_name_throw() {
-//     assert!(parse_query(
-//         "match\n$x isa name;
-// get; hello $x;"
-//     )
-//     .is_err());
-// }
-
-#[test]
-fn define_attribute_type_regex() {
-    let query = r#"define
-digit sub attribute,
-    value string @regex("\d");"#;
-
-    let parsed = parse_query(query).unwrap().into_schema().into_define();
-    let expected = typeql_define!(/*type_("digit").sub("attribute").regex(r"\d")*/);
-
-    assert_valid_eq_repr!(expected, parsed, query);
-}
-
-// #[test]
-// fn undefine_attribute_type_regex() {
-//     let query = r#"undefine
-// digit regex "\d";"#;
-
-//     let parsed = parse_query(query).unwrap().into_schema().into_undefine();
-//     let expected = typeql_undefine!(type_("digit").regex(r"\d"));
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn regex_predicate_parses_character_classes_correctly() {
-//     let query = r#"match
-// $x like "\d";
-// get;"#;
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").like("\\d")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn regex_predicate_parses_quotes_correctly() {
-//     let query = r#"match
-// $x like "\"";
-// get;"#;
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").like("\\\"")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn regex_predicate_parses_backslashes_correctly() {
-//     let query = r#"match
-// $x like "\\";
-// get;"#;
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").like("\\\\")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn regex_predicate_parses_newline_correctly() {
-//     let query = r#"match
-// $x like "\n";
-// get;"#;
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").like("\\n")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn regex_predicate_parses_forward_slashes_correctly() {
-//     let query = r#"match
-// $x like "\/";
-// get;"#;
-//     let parsed = parse_query(query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").like("/")).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_value_equality_to_string_create_valid_query_string() {
-//     let expected = typeql_match!(cvar("x").eq(cvar("y"))).get();
-//     let parsed = parse_query(&expected.to_string()).unwrap().into_get();
-
-//     assert_eq!(expected, parsed);
-// }
-
-// #[test]
-// fn test_iid_constraint() {
-//     let iid = "0x0123456789abcdefdeadbeef";
-//     let query = format!(
-//         r#"match
-// $x iid {iid};
-// get;"#
-//     );
-
-//     let parsed = parse_query(&query).unwrap().into_get();
-//     let expected = typeql_match!(cvar("x").iid(iid)).get();
-//     assert_valid_eq_repr!(expected, parsed, query);
-// }
-
-// #[test]
-// fn when_parsing_invalid_iid_throw() {
-//     let iid = "invalid";
-//     let query = format!(
-//         r#"match
-// $x iid {iid};
-// get;"#
-//     );
-
-//     let parsed = parse_query(&query);
-//     assert!(parsed.is_err());
-// }
-
-// #[test]
-// fn when_building_invalid_iid_throw() {
-//     let iid = "invalid";
-//     let expected = typeql_match!(cvar("x").iid(iid)).get().validated();
-//     assert!(expected.is_err());
-// }
-
-// #[test]
-// fn test_utf8_variable() {
-//     let var = "人";
-//     let expected = typeql_match!(cvar(var).isa("person")).get().validated();
-//     assert!(expected.is_ok());
-// }
-
-// #[test]
-// fn when_using_invalid_variable_throw() {
-//     let var = "_人";
-//     let expected = typeql_match!(cvar(var).isa("person")).get().validated();
-//     assert!(expected.is_err());
-// }
-
-// #[test]
-// fn test_utf8_label() {
-//     let label = "人";
-//     let expected = typeql_match!(cvar("x").isa(label)).get().validated();
-//     assert!(expected.is_ok());
-// }
-
-// #[test]
-// fn test_utf8_value() {
-//     let value = "人";
-//     let expected = typeql_match!(cvar("x").isa("person").has(("name", value))).get().validated();
-//     assert!(expected.is_ok());
-// }
diff --git a/rust/parser/test/modifiers.rs b/rust/parser/test/modifiers.rs
new file mode 100644
index 000000000..4689dbf74
--- /dev/null
+++ b/rust/parser/test/modifiers.rs
@@ -0,0 +1,94 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_get_sort_on_concept_variable() {
+    let query = r#"match
+$x isa movie,
+    has rating $r;
+sort $r desc;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("movie").has(("rating", cvar("r")))).sort([(cvar("r"), Desc)]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_get_sort_on_value_variable() {
+    let query = r#"match
+$x isa movie,
+    has rating $r;
+$l = 100 - $r;
+sort $l desc;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("movie").has(("rating", cvar("r"))),
+    //         var("l").assign(constant(100).subtract(cvar("r")))
+    //     )
+    //
+    //     .sort([(var("l"), Desc)]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_get_sort_multiple() {
+    let query = r#"match
+$x isa movie,
+    has title $t,
+    has rating $r;
+$rate = $r * 100;
+sort $rate desc, $t;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(
+    //         var("x").isa("movie").has(("title", cvar("t"))).has(("rating", cvar("r"))),
+    //         var("rate").assign(cvar("r").multiply(100)),
+    //     )
+    //
+    //     .sort(sort_vars!((var("rate"), Desc), cvar("t")));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_get_sort_limit() {
+    let query = r#"match
+$x isa movie,
+    has rating $r;
+sort $r;
+limit 10;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("movie").has(("rating", cvar("r")))).sort([cvar("r")]).limit(10);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_get_sort_offset_limit() {
+    let query = r#"match
+$x isa movie,
+    has rating $r;
+sort $r desc;
+offset 10;
+limit 10;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("movie").has(("rating", cvar("r"))))
+    //         .sort([(var("r"), Desc)])
+    //         .offset(10)
+    //         .limit(10);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_get_offset_limit() {
+    let query = r#"match
+$y isa movie,
+    has title $n;
+offset 2;
+limit 4;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("y").isa("movie").has(("title", cvar("n")))).offset(2).limit(4);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/nonquery.rs b/rust/parser/test/nonquery.rs
new file mode 100644
index 000000000..a3e6ff0b2
--- /dev/null
+++ b/rust/parser/test/nonquery.rs
@@ -0,0 +1,79 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+// #[test]
+// fn test_parsing_pattern() {
+//     let pattern = r#"{
+//     (wife: $a, husband: $b) isa marriage;
+//     $a has gender "male";
+//     $b has gender "female";
+// }"#;
+//     let parsed = parse_pattern(pattern).unwrap().into_conjunction();
+//     let expected = and!(
+//         rel(("wife", "a")).links(("husband", "b")).isa("marriage"),
+//         var("a").has(("gender", "male")),
+//         var("b").has(("gender", "female"))
+//     );
+//     assert_valid_eq_repr!(expected, parsed, pattern);
+// }
+
+// #[test]
+// fn test_parsing_patterns() {
+//     let patterns = r#"(wife: $a, husband: $b) isa marriage;
+//     $a has gender "male";
+//     $b has gender "female";
+// "#;
+//     let parsed = parse_patterns(patterns).unwrap().into_iter().map(|p| p.into_statement()).collect::<Vec<_>>();
+//     let expected: Vec<Statement> = vec![
+//         Statement::Thing(rel(("wife", "a")).links(("husband", "b")).isa("marriage")),
+//         Statement::Thing(var("a").has(("gender", "male"))),
+//         Statement::Thing(var("b").has(("gender", "female"))),
+//     ];
+//     assert_eq!(expected, parsed);
+// }
+
+// #[test]
+// fn test_parsing_definables() {
+//     let query = r#"athlete sub person;
+//       runner sub athlete;
+//       sprinter sub runner;"#;
+//     let parsed = parse_definables(query).unwrap().into_iter().map(|p| p.into_type_statement()).collect::<Vec<_>>();
+//     let expected =
+//         vec![type_("athlete").sub("person"), type_("runner").sub("athlete"), type_("sprinter").sub("runner")];
+//     assert_eq!(expected, parsed);
+// }
+
+// #[test]
+// fn test_parsing_variable_rel() {
+//     let variable = "(wife: $a, husband: $b) isa marriage";
+//     let parsed = parse_statement(variable).unwrap();
+//     if let Statement::Thing(parsed_var) = parsed {
+//         let expected = rel(("wife", "a")).links(("husband", "b")).isa("marriage");
+//         assert_valid_eq_repr!(expected, parsed_var, variable);
+//     } else {
+//         panic!("Expected ThingVariable, found {variable:?}.");
+//     }
+// }
+
+// #[test]
+// fn test_parsing_variable_has() {
+//     let variable = "$x has is_interesting true";
+//     let parsed = parse_statement(variable).unwrap();
+//     if let Statement::Thing(parsed_var) = parsed {
+//         let expected = var("x").has(("is_interesting", true));
+//         assert_valid_eq_repr!(expected, parsed_var, variable);
+//     } else {
+//         panic!("Expected ThingVariable, found {variable:?}.");
+//     }
+// }
+
+// #[test]
+// fn test_parsing_label() {
+//     let label = "label_with-symbols";
+//     let parsed = parse_label(label).unwrap();
+//     let expected = Label { scope: None, name: String::from(label) };
+//     assert_eq!(expected, parsed);
+// }
diff --git a/rust/parser/test/regex.rs b/rust/parser/test/regex.rs
new file mode 100644
index 000000000..fb7278183
--- /dev/null
+++ b/rust/parser/test/regex.rs
@@ -0,0 +1,53 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn regex_predicate_parses_character_classes_correctly() {
+    let query = r#"match
+$x like "\d";"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").like(r"\d"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn regex_predicate_parses_quotes_correctly() {
+    let query = r#"match
+$x like "\"";"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").like(r#"\""#));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn regex_predicate_parses_backslashes_correctly() {
+    let query = r#"match
+$x like "\\";"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").like(r"\\"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn regex_predicate_parses_newline_correctly() {
+    let query = r#"match
+$x like "\n";"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").like(r"\n"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn regex_predicate_parses_forward_slashes_correctly() {
+    let query = r#"match
+$x like "\/";"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = match_!(var("x").like("/"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/schema_queries.rs b/rust/parser/test/schema_queries.rs
new file mode 100644
index 000000000..aeeb4e836
--- /dev/null
+++ b/rust/parser/test/schema_queries.rs
@@ -0,0 +1,186 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_define_query_with_owns_overrides() {
+    let query = r#"define
+triangle sub entity;
+triangle owns side-length;
+triangle-right-angled sub triangle;
+triangle-right-angled owns hypotenuse-length as side-length;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(
+    // type_("triangle").sub("entity"),
+    // type_("triangle").owns("side-length"),
+    // type_("triangle-right-angled").sub("triangle"),
+    // type_("triangle-right-angled").owns("hypotenuse-length".as_("side-length"))
+    // );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_define_query_with_relates_overrides() {
+    let query = r#"define
+pokemon sub entity;
+evolves sub relation;
+evolves relates from,
+    relates to;
+evolves-final sub evolves;
+evolves-final relates from-final as from;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(
+    // type_("pokemon").sub("entity"),
+    // type_("evolves").sub("relation"),
+    // type_("evolves").relates("from").relates("to"),
+    // type_("evolves-final").sub("evolves"),
+    // type_("evolves-final").relates("from-final".as_("from"))
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_define_query_with_plays_overrides() {
+    let query = r#"define
+pokemon sub entity;
+evolves sub relation;
+evolves relates from,
+    relates to;
+evolves-final sub evolves;
+evolves-final relates from-final as from;
+pokemon plays evolves-final:from-final as from;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(
+    // type_("pokemon").sub("entity"),
+    // type_("evolves").sub("relation"),
+    // type_("evolves").relates("from").relates("to"),
+    // type_("evolves-final").sub("evolves"),
+    // type_("evolves-final").relates("from-final".as_("from").distinct()),
+    // type_("pokemon")
+    // .plays("evolves-final:from-final".ordered())
+    // .distinct()
+    // .card(0, None)
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_define_query() {
+    let query = r#"define
+pokemon sub entity;
+evolution sub relation;
+evolves-from sub role;
+evolves-to sub role;
+evolves relates from,
+    relates to;
+pokemon plays evolves:from,
+    plays evolves:to,
+    owns name;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(
+    // type_("pokemon").sub("entity"),
+    // type_("evolution").sub("relation"),
+    // type_("evolves-from").sub("role"),
+    // type_("evolves-to").sub("role"),
+    // type_("evolves").relates("from").relates("to"),
+    // type_("pokemon").plays(("evolves", "from")).plays(("evolves", "to")).owns("name")
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_undefine_query_result_is_same_as_java_typeql() {
+    let query = r#"undefine
+pokemon;
+evolution;
+evolves-from;
+evolves-to;
+relates from from evolves;
+relates to from evolves;
+plays evolves:from from pokemon;
+plays evolves:to from pokemon;
+owns name from pokemon;"#;
+
+    let parsed = parse_query(query).unwrap();
+    // let expected = undefine!(
+    // type_("pokemon").sub("entity"),
+    // type_("evolution").sub("relation"),
+    // type_("evolves-from").sub("role"),
+    // type_("evolves-to").sub("role"),
+    // type_("evolves").relates("from").relates("to"),
+    // type_("pokemon").plays(("evolves", "from")).plays(("evolves", "to")).owns("name")
+    // );
+
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_define_abstract_entity_query() {
+    let query = r#"define
+concrete-type sub entity;
+abstract-type sub entity @abstract;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(type_("concrete-type").sub("entity"), type_("abstract-type").sub("entity"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_define_value_type_query() {
+    let query = r#"define
+my-type sub attribute,
+    value long;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(type_("my-type").sub("attribute").value(ValueType::Long));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn define_attribute_type_regex() {
+    let query = r#"define
+digit sub attribute,
+    value string @regex("\d");"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = define!(type_("digit").sub("attribute").regex(r"\d"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn when_parsing_as_in_define_result_is_same_as_sub() {
+    let query = r#"define
+parent sub role;
+child sub role;
+parenthood sub relation,
+    relates parent,
+    relates child;
+fatherhood sub parenthood,
+    relates father as parent,
+    relates son as child;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_define!(
+    //         type_("parent").sub("role"),
+    //         type_("child").sub("role"),
+    //         type_("parenthood").sub("relation").relates("parent").relates("child"),
+    //         type_("fatherhood").sub("parenthood").relates(("father", "parent")).relates(("son", "child"))
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn undefine_attribute_type_regex() {
+    let query = r#"undefine
+@regex from digit value string;"#;
+    let parsed = parse_query(query).unwrap();
+    // let expected = undefine!(type_("digit").regex(r"\d"));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
diff --git a/rust/parser/test/write_queries.rs b/rust/parser/test/write_queries.rs
new file mode 100644
index 000000000..941960206
--- /dev/null
+++ b/rust/parser/test/write_queries.rs
@@ -0,0 +1,72 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
+ */
+
+use super::assert_valid_eq_repr;
+use crate::parse_query;
+
+#[test]
+fn test_delete_query() {
+    let query = r#"match
+$x isa movie,
+    has title "The Title";
+$y isa movie,
+    has title $t;
+delete
+$x;
+has $t of $y;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("movie").has(("title", "The Title")), cvar("y").isa("movie"))
+    //         .delete([var("x").isa("movie"), cvar("y").isa("movie")]);
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_insert_query() {
+    let query = r#"insert
+$x isa pokemon,
+    has name "Pichu";
+$y isa pokemon,
+    has name "Pikachu";
+$z isa pokemon,
+    has name "Raichu";
+(evolves-from: $x, evolves-to: $y) isa evolution;
+(evolves-from: $y, evolves-to: $z) isa evolution;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_insert!(
+    //         var("x").isa("pokemon").has(("name", "Pichu")),
+    //         var("y").isa("pokemon").has(("name", "Pikachu")),
+    //         var("z").isa("pokemon").has(("name", "Raichu")),
+    //         rel(("evolves-from", "x")).links(("evolves-to", "y")).isa("evolution"),
+    //         rel(("evolves-from", "y")).links(("evolves-to", "z")).isa("evolution")
+    //     );
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_update_query() {
+    let query = r#"match
+$x isa person,
+    has name "alice",
+    has age $a;
+update
+$x has age 25;"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("person").has(("name", "alice")).has(("age", cvar("a"))))
+    //         .delete(var("x").has(cvar("a")))
+    //         .insert(var("x").has(("age", 25)));
+    assert_valid_eq_repr!(expected, parsed, query);
+}
+
+#[test]
+fn test_match_insert_query() {
+    let query = r#"match
+$x isa language;
+insert
+$x has name "HELLO";"#;
+    let parsed = parse_query(query).unwrap();
+    //     let expected = typeql_match!(var("x").isa("language")).insert(cvar("x").has(("name", "HELLO")));
+    assert_valid_eq_repr!(expected, parsed, query);
+}