Repository: rcoh/angle-grinder
Pull request #148 (2021-08-02 04:44:15)

[lang] migrate to nom 6
!THIS IS NOT COMPLETE! I'm mostly looking for some high-level
feedback at the moment.  Most of the tests are passing, except
for the alias-related ones.

This change migrates the lang module from nom 4 to nom 6 (the
latest release at the time).  I tried to follow the existing
code for the most part.  I didn't want to deviate too much
so that we could still compare the two impls and not introduce
too many new bugs.  The changes I have introduced are to try
and improve error handling.  I tried to follow the advice in
this post:

  https://eyalkalderon.com/blog/nom-error-recovery/

Basically, the parse should always succeed and errors are
inserted into the AST (e.g. a bad expression is represented
as an Expr::Error in the tree).  Error messages are logged
using the existing ErrorReporter mechanism.  In addition,
when unexpected data is encountered, the parser tries to
seek forward to the next section of the pipeline (i.e. the
vertical bar).

TODO:
 * Implement aliases
 * Try to handle more error cases
 * Remove dead code
 * Double check that old parser behaviors are preserved


tstack: I think the priority would be defined by these lines:

https://github.com/rcoh/angle-grinder/pull/148/files#diff-9184a440fdb6af807da5a90cdcd5865c62abef73df0a89c2890129ee9e9a011cR1371-R1379

The inline/aggregate operators (`inline_opers`, `multi_agg_opers`) should be tried before plain expressions `field_expr`.  So, I think it should be working, but it would be good to find an example that failed previously.  This version of the code reworked the `did_you_mean` stuff which made things a bit awkward in the previous version.
tstack: > The `<xyz>_n` functions

Sorry, I'm not sure what you're referring to here... do you mean things like `arg1` ?
rcoh: yeah delimited_2 delimited_3 etc.
tstack: > yeah delimited_2

I've renamed `delimited2` to `expect_delimited()`.  I started with `delimited2()` since it was a new version of the provided `delimited()` combinator that had some extra error handling machinery.

I also changed `arg1` to `single_arg()` since there's a a lot of operators that just take a single argument.

This latest update gets aliases working again.  I changed the alias implementation around a bit so that the alias parsing happens during static initialization instead of when it was used.

I don't really have any more plans of things to change in this pull (beyond your comments).  Hopefully, this reaches parity the the current state of the world.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 4cda5c9..01bbe20 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "addr2line"
 version = "0.15.1"
@@ -24,9 +26,11 @@ dependencies = [
  "atty",
  "cargo-husky",
  "chrono",
+ "cool_asserts",
  "criterion",
  "crossbeam-channel 0.3.9",
  "dtparse",
+ "expect-test",
  "failure",
  "human-panic",
  "im",
@@ -37,9 +41,8 @@ dependencies = [
  "logfmt",
  "maplit",
  "nom",
+ "nom-supreme",
  "nom_locate",
- "num-derive",
- "num-traits",
  "ordered-float",
  "predicates",
  "pulldown-cmark",
@@ -68,11 +71,12 @@ dependencies = [
 
 [[package]]
 name = "annotate-snippets"
-version = "0.5.0"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8bcdcd5b291ce85a78f2b9d082a8de9676c12b1840d386d67bc5eea6f9d2b4e"
+checksum = "5c96c3d1062ea7101741480185a6a1275eab01cbe8b20e378d1311bc056d2e08"
 dependencies = [
- "ansi_term",
+ "unicode-width",
+ "yansi-term",
 ]
 
 [[package]]
@@ -84,6 +88,18 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
+
+[[package]]
+name = "arrayvec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
+
 [[package]]
 name = "assert_cmd"
 version = "1.0.4"
@@ -158,6 +174,18 @@ version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
 
+[[package]]
+name = "bitvec"
+version = "0.19.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
 [[package]]
 name = "bstr"
 version = "0.2.16"
@@ -178,9 +206,9 @@ checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
 
 [[package]]
 name = "bytecount"
-version = "0.3.2"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f861d9ce359f56dbcb6e0c2a1cb84e52ad732cadb57b806adeb3c7668caccbd8"
+checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
 
 [[package]]
 name = "byteorder"
@@ -216,6 +244,12 @@ version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7b02b629252fe8ef6460461409564e2c21d0c8e77e0944f3d189ff06c4e932ad"
 
+[[package]]
+name = "cascade"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f18c6a921baae2d947e4cf96f6ef1b5774b3056ae8edbdf5c5cfce4f33260921"
+
 [[package]]
 name = "cast"
 version = "0.2.6"
@@ -308,6 +342,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "cool_asserts"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38ebc05dfb52d8103136796ad7d9d95d9f39d8ddec4f55249d40667d245f6486"
+
 [[package]]
 name = "criterion"
 version = "0.3.4"
@@ -436,6 +476,12 @@ version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
 
+[[package]]
+name = "dissimilar"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc4b29f4b9bb94bf267d57269fd0706d343a160937108e9619fe380645428abb"
+
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -498,6 +544,16 @@ dependencies = [
  "failure",
 ]
 
+[[package]]
+name = "expect-test"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2300477aab3a378f2ca00a4fbd4dc713654ab7ed790e4017493cb33656280633"
+dependencies = [
+ "dissimilar",
+ "once_cell",
+]
+
 [[package]]
 name = "failure"
 version = "0.1.8"
@@ -567,6 +623,12 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
 
+[[package]]
+name = "funty"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
+
 [[package]]
 name = "futures-channel"
 version = "0.3.15"
@@ -595,7 +657,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121"
 dependencies = [
  "autocfg",
- "proc-macro-hack 0.5.19",
+ "proc-macro-hack",
  "proc-macro2 1.0.26",
  "quote 1.0.9",
  "syn 1.0.72",
@@ -627,7 +689,7 @@ dependencies = [
  "memchr",
  "pin-project-lite 0.2.6",
  "pin-utils",
- "proc-macro-hack 0.5.19",
+ "proc-macro-hack",
  "proc-macro-nested",
  "slab",
 ]
@@ -658,12 +720,6 @@ version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189"
 
-[[package]]
-name = "glob"
-version = "0.2.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
-
 [[package]]
 name = "glob"
 version = "0.3.0"
@@ -899,28 +955,34 @@ dependencies = [
 
 [[package]]
 name = "include_dir"
-version = "0.2.1"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f41a8bee1894b3fb755d8f09ccd764650476358197a0582555f698fe84b0ae93"
+checksum = "31a924bd335356c7622dff9ee33d06920afcf7f762a1a991236645e08c8a484b"
 dependencies = [
- "glob 0.2.11",
+ "glob",
  "include_dir_impl",
- "proc-macro-hack 0.4.3",
+ "proc-macro-hack",
 ]
 
 [[package]]
 name = "include_dir_impl"
-version = "0.2.1"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c4b029199aef0fb9921fdc5623843197e6f4a035774523817599a9f55e4bf3b"
+checksum = "afae3917f781921d7c7813992ccadff7e816f7e6ecb4b70a9ec3e740d51da3d6"
 dependencies = [
- "failure",
- "proc-macro-hack 0.4.3",
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "syn 0.14.9",
+ "anyhow",
+ "proc-macro-hack",
+ "proc-macro2 1.0.26",
+ "quote 1.0.9",
+ "syn 1.0.72",
 ]
 
+[[package]]
+name = "indent_write"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3"
+
 [[package]]
 name = "indexmap"
 version = "1.6.2"
@@ -1012,6 +1074,12 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "joinery"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5"
+
 [[package]]
 name = "js-sys"
 version = "0.3.51"
@@ -1043,6 +1111,19 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lexical-core"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
+dependencies = [
+ "arrayvec",
+ "bitflags",
+ "cfg-if 1.0.0",
+ "ryu",
+ "static_assertions",
+]
+
 [[package]]
 name = "libc"
 version = "0.2.94"
@@ -1161,19 +1242,35 @@ dependencies = [
 
 [[package]]
 name = "nom"
-version = "4.2.3"
+version = "6.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
+checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
 dependencies = [
+ "bitvec",
+ "funty",
+ "lexical-core",
  "memchr",
- "version_check 0.1.5",
+ "version_check",
+]
+
+[[package]]
+name = "nom-supreme"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "133b9264602f2a75dcc5e3e39f3f7f98c6cee76f2cc749b9d8e751ffe12a712f"
+dependencies = [
+ "cascade",
+ "indent_write",
+ "joinery",
+ "memchr",
+ "nom",
 ]
 
 [[package]]
 name = "nom_locate"
-version = "0.3.1"
+version = "3.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a47c112b3861d81f7fbf73892b9271af933af32bd5dee6889aa3c3fa9caed7e"
+checksum = "302954611858a07e00f85110b4ea61859dea77d63f109bd60ab7b8eb533bbacb"
 dependencies = [
  "bytecount",
  "memchr",
@@ -1221,17 +1318,6 @@ dependencies = [
  "num-traits",
 ]
 
-[[package]]
-name = "num-derive"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eafd0b45c5537c3ba526f79d3e75120036502bebacbb3f3220914067ce39dbf2"
-dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "syn 0.15.44",
-]
-
 [[package]]
 name = "num-integer"
 version = "0.1.44"
@@ -1457,27 +1543,12 @@ dependencies = [
  "treeline",
 ]
 
-[[package]]
-name = "proc-macro-hack"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7f95648580798cc44ff8efb9bb0d7ee5205ea32e087b31b0732f3e8c2648ee2"
-dependencies = [
- "proc-macro-hack-impl",
-]
-
 [[package]]
 name = "proc-macro-hack"
 version = "0.5.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
 
-[[package]]
-name = "proc-macro-hack-impl"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7be55bf0ae1635f4d7c7ddd6efc05c631e98a82104a73d35550bbc52db960027"
-
 [[package]]
 name = "proc-macro-nested"
 version = "0.1.7"
@@ -1569,6 +1640,12 @@ dependencies = [
  "proc-macro2 1.0.26",
 ]
 
+[[package]]
+name = "radium"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
+
 [[package]]
 name = "rand"
 version = "0.8.3"
@@ -1951,6 +2028,12 @@ version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
 [[package]]
 name = "strfmt"
 version = "0.1.6"
@@ -1985,17 +2068,6 @@ dependencies = [
  "syn 0.15.44",
 ]
 
-[[package]]
-name = "syn"
-version = "0.14.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261ae9ecaa397c42b960649561949d69311f08eeaea86a65696e6e46517cf741"
-dependencies = [
- "proc-macro2 0.4.30",
- "quote 0.6.13",
- "unicode-xid 0.1.0",
-]
-
 [[package]]
 name = "syn"
 version = "0.15.44"
@@ -2030,6 +2102,12 @@ dependencies = [
  "unicode-xid 0.2.2",
 ]
 
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
 [[package]]
 name = "tempfile"
 version = "3.2.0"
@@ -2069,7 +2147,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea97be90349ab3574f6e74d1566e1c5dd3a4bc74b89f4af4cc10ca010af103c0"
 dependencies = [
- "glob 0.3.0",
+ "glob",
  "proc-macro2 0.4.30",
  "quote 0.6.13",
  "syn 0.15.44",
@@ -2257,7 +2335,7 @@ version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
 dependencies = [
- "version_check 0.9.3",
+ "version_check",
 ]
 
 [[package]]
@@ -2335,12 +2413,6 @@ version = "0.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
 
-[[package]]
-name = "version_check"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
-
 [[package]]
 name = "version_check"
 version = "0.9.3"
@@ -2541,3 +2613,18 @@ dependencies = [
  "winapi 0.2.8",
  "winapi-build",
 ]
+
+[[package]]
+name = "wyz"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
+
+[[package]]
+name = "yansi-term"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
+dependencies = [
+ "winapi 0.3.9",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 0dcc7e6..9d1244b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,8 +25,9 @@ self-update = ["self_update"]
 jemallocator = "0.3.2"
 serde_json = "1.0.33"
 itertools = "0.8.0"
-nom = { version = "4.2.3", features = ["verbose-errors"] }
-nom_locate = { version = "0.3.1", features = ["verbose-errors"] }
+nom = "6"
+nom_locate = "3"
+nom-supreme = "0.4.4"
 strsim = "0.8.0"
 quicli = "0.4.0"
 regex = "1.1.0"
@@ -38,15 +39,13 @@ failure = "0.1.3"
 structopt = "0.2"
 human-panic = "1.0.1"
 self_update = { version = "0.19.0", features = ["rustls"], default_features = false, optional = true }
-num-derive = "0.2.3"
-num-traits = "0.2.6"
-annotate-snippets = { version = "0.5.0", features = ["ansi_term"] }
+annotate-snippets = { version = "0.9.0", features = ["color"] }
 atty = "0.2.0"
 lazy_static = "1.2.0"
 im = "13.0.0"
 logfmt = "0.0.2"
 strfmt = "0.1.6"
-include_dir = "0.2.1"
+include_dir = "0.6.1"
 toml = "0.4.10"
 serde = { version = "1.0", features = ["derive"] }
 chrono = "0.4"
@@ -54,6 +53,8 @@ dtparse = "1.1"
 
 [dev-dependencies]
 assert_cmd = "1.0.1"
+cool_asserts = "1.1.0"
+expect-test = "1.1.0"
 predicates = "1.0.5"
 pulldown-cmark = "0.2.0"
 criterion = "0.3"
diff --git a/README.md b/README.md
index f06f6bb..e91f130 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+
 # angle-grinder [![Build Status](https://travis-ci.org/rcoh/angle-grinder.svg?branch=main)](https://travis-ci.org/rcoh/angle-grinder) [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/angle-grinder/Lobby)
 Slice and dice log files on the command line.
 
diff --git a/src/alias.rs b/src/alias.rs
index 9fecc12..a43260c 100644
--- a/src/alias.rs
+++ b/src/alias.rs
@@ -7,17 +7,28 @@
 
 use lazy_static::lazy_static;
 
+use crate::errors::{QueryContainer, TermErrorReporter};
+use crate::lang::{pipeline_template, Operator};
 use include_dir::Dir;
 use serde::Deserialize;
 
 const ALIASES_DIR: Dir = include_dir!("aliases");
 
 lazy_static! {
-    pub static ref LOADED_ALIASES: Vec<AliasConfig> = ALIASES_DIR
+    pub static ref LOADED_ALIASES: Vec<AliasPipeline> = ALIASES_DIR
         .files()
         .iter()
         .map(|file| {
-            toml::from_str(file.contents_utf8().expect("load string")).expect("toml valid")
+            let config: AliasConfig =
+                toml::from_str(file.contents_utf8().expect("load string")).expect("toml valid");
+            let reporter = Box::new(TermErrorReporter {});
+            let qc = QueryContainer::new(config.template, reporter);
+            let pipeline = pipeline_template(&qc).expect("valid alias");
+
+            AliasPipeline {
+                keyword: config.keyword,
+                pipeline,
+            }
         })
         .collect();
     pub static ref LOADED_KEYWORDS: Vec<&'static str> =
@@ -30,21 +41,22 @@ pub struct AliasConfig {
     template: String,
 }
 
-impl AliasConfig {
-    pub fn matching_string(s: String) -> Result<&'static AliasConfig, ()> {
-        for alias in LOADED_ALIASES.iter() {
-            if alias.keyword != s {
-                continue;
-            }
-
-            return Ok(alias);
-        }
+#[derive(Debug)]
+pub struct AliasPipeline {
+    keyword: String,
+    pipeline: Vec<Operator>,
+}
 
-        Err(())
+impl AliasPipeline {
+    pub fn matching_string(s: &str) -> Result<&'static AliasPipeline, ()> {
+        LOADED_ALIASES
+            .iter()
+            .find(|alias| alias.keyword == s)
+            .ok_or(())
     }
 
     /// Render the alias as a string that should parse into a valid operator.
-    pub fn render(&self) -> String {
-        self.template.clone()
+    pub fn render(&self) -> Vec<Operator> {
+        self.pipeline.clone()
     }
 }
diff --git a/src/bin/agrind.rs b/src/bin/agrind.rs
index 41ca90b..beebd4f 100644
--- a/src/bin/agrind.rs
+++ b/src/bin/agrind.rs
@@ -1,12 +1,9 @@
-use ag::pipeline::{ErrorReporter, OutputMode, Pipeline, QueryContainer};
-use annotate_snippets::snippet::Snippet;
-use atty::Stream;
+use ag::pipeline::{OutputMode, Pipeline, QueryContainer, TermErrorReporter};
 use human_panic::setup_panic;
 use quicli::prelude::*;
 
 #[cfg(feature = "self_update")]
 use self_update;
-use std::env;
 use std::fs::File;
 use std::io;
 use std::io::{stdout, BufReader};
@@ -79,19 +76,6 @@ pub enum InvalidArgs {
     CantSupplyBoth,
 }
 
-/// An ErrorReporter that writes errors related to the query string to the terminal
-struct TermErrorReporter {
-    formatter: annotate_snippets::formatter::DisplayListFormatter,
-}
-
-impl ErrorReporter for TermErrorReporter {
-    fn handle_error(&self, snippet: Snippet) {
-        let dl = annotate_snippets::display_list::DisplayList::from(snippet);
-
-        eprintln!("{}", self.formatter.format(&dl));
-    }
-}
-
 fn main() -> CliResult {
     setup_panic!();
     let args = Cli::from_args();
@@ -101,11 +85,7 @@ fn main() -> CliResult {
     }
     let query = QueryContainer::new(
         args.query.ok_or(InvalidArgs::MissingQuery)?,
-        Box::new(TermErrorReporter {
-            formatter: annotate_snippets::formatter::DisplayListFormatter::new(
-                env::var("NO_COLOR").is_err() && atty::is(Stream::Stderr),
-            ),
-        }),
+        Box::new(TermErrorReporter {}),
     );
     args.verbosity.setup_env_logger("agrind")?;
     let output_mode = match (args.output, args.format) {
diff --git a/src/errors.rs b/src/errors.rs
index 71c4b28..828f23d 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,122 +1,46 @@
-use crate::lang::{
-    query, Positioned, Query, QueryPosition, Span, VALID_AGGREGATES, VALID_INLINE, VALID_OPERATORS,
-};
+use crate::lang::{query, Positioned, Query};
 use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
-use failure::Fail;
-use nom::types::CompleteStr;
-use nom::ErrorKind;
-use num_derive::FromPrimitive;
-use num_traits::FromPrimitive;
-use std::convert::From;
+use atty::Stream;
+use std::env;
+use std::ops::Range;
+use std::sync::atomic::{AtomicUsize, Ordering};
 use strsim::normalized_levenshtein;
 
 /// Container for the query string that can be used to parse and report errors.
 pub struct QueryContainer {
-    query: String,
-    reporter: Box<dyn ErrorReporter>,
-}
-
-/// Common syntax errors.
-#[derive(PartialEq, Debug, FromPrimitive, Fail, Clone)]
-pub enum SyntaxErrors {
-    #[fail(display = "")]
-    StartOfError,
-    #[fail(display = "unterminated single quoted string")]
-    UnterminatedSingleQuotedString,
-    #[fail(display = "unterminated double quoted string")]
-    UnterminatedDoubleQuotedString,
-    #[fail(display = "expecting an operand for binary operator")]
-    MissingOperand,
-    #[fail(display = "expecting a name for the field")]
-    MissingName,
-    #[fail(display = "expecting close parentheses")]
-    MissingParen,
-
-    #[fail(display = "Expected an operator")]
-    NotAnOperator,
-
-    #[fail(display = "Not an aggregate operator")]
-    NotAnAggregateOperator,
-
-    #[fail(display = "Invalid filter")]
-    InvalidFilter,
-
-    #[fail(display = "Invalid boolean expression")]
-    InvalidBooleanExpression,
+    pub query: String,
+    pub reporter: Box<dyn ErrorReporter>,
+    pub error_count: AtomicUsize,
 }
 
 /// Trait that can be used to report errors by the parser and other layers.
 pub trait ErrorBuilder {
     /// Create a SnippetBuilder for the given error
     fn report_error_for<E: ToString>(&self, error: E) -> SnippetBuilder;
+
+    fn get_error_count(&self) -> usize;
 }
 
 impl QueryContainer {
     pub fn new(query: String, reporter: Box<dyn ErrorReporter>) -> QueryContainer {
-        QueryContainer { query, reporter }
+        QueryContainer {
+            query,
+            reporter,
+            error_count: AtomicUsize::new(0),
+        }
     }
 
     /// Parse the contained query string.
-    pub fn parse(&self) -> Result<Query, QueryPosition> {
-        let parse_result = query(Span::new(CompleteStr(&self.query)));
-
-        let errors = match parse_result {
-            Err(nom::Err::Failure(nom::Context::List(ref list))) => Some(list),
-            Err(nom::Err::Error(nom::Context::List(ref list))) => Some(list),
-            _ => None,
-        };
-        if let Some(ref list) = errors {
-            // Check for an error from a delimited!() parser.  The error list will contain
-            // the location of the start as the last item and the location of the end as the
-            // penultimate item.
-            let last_chunk = list.rchunks_exact(2).next().map(|p| (&p[0], &p[1]));
-
-            match last_chunk {
-                Some((
-                    (ref end_span, ErrorKind::Custom(ref delim_error)),
-                    (ref start_span, ErrorKind::Custom(SyntaxErrors::StartOfError)),
-                )) => {
-                    self.report_error_for(delim_error)
-                        .with_code_range((*start_span).into(), (*end_span).into(), "")
-                        .with_resolutions(
-                            delim_error
-                                .to_resolution(&(&self.query)[start_span.offset..end_span.offset]),
-                        )
-                        .send_report();
-                }
-                _ => {
-                    list.iter().for_each(|(span, error)| match error {
-                        ErrorKind::Custom(ref custom_error) => self
-                            .report_error_for(custom_error)
-                            .with_code_range(
-                                QueryPosition(span.offset),
-                                QueryPosition(span.offset + span.fragment.len()),
-                                "",
-                            )
-                            .with_resolutions(custom_error.to_resolution(
-                                &(&self.query)[span.offset..span.offset + span.fragment.len()],
-                            ))
-                            .send_report(),
-                        _other => (),
-                    });
-                }
-            }
-        };
-
-        // Return the parsed value or the last position of valid syntax
-        parse_result.map(|x| x.1).map_err(|e| match e {
-            nom::Err::Incomplete(_) => QueryPosition(0),
-            nom::Err::Error(context) | nom::Err::Failure(context) => match context {
-                nom::Context::Code(span, _) => span.into(),
-                nom::Context::List(list) => list.first().unwrap().0.into(),
-            },
-        })
+    pub fn parse(&self) -> Result<Query, ()> {
+        query(self)
     }
 }
 
 impl ErrorBuilder for QueryContainer {
     /// Create a SnippetBuilder for the given error
     fn report_error_for<E: ToString>(&self, error: E) -> SnippetBuilder {
+        self.error_count.fetch_add(1, Ordering::Relaxed);
+
         SnippetBuilder {
             query: self,
             data: SnippetData {
@@ -126,9 +50,13 @@ impl ErrorBuilder for QueryContainer {
             },
         }
     }
+
+    fn get_error_count(&self) -> usize {
+        self.error_count.load(Ordering::Relaxed)
+    }
 }
 
-fn did_you_mean(input: &str, choices: &[&str]) -> Option<String> {
+pub fn did_you_mean(input: &str, choices: &[&str]) -> Option<String> {
     let similarities = choices
         .iter()
         .map(|choice| (choice, normalized_levenshtein(choice, input)));
@@ -140,76 +68,21 @@ fn did_you_mean(input: &str, choices: &[&str]) -> Option<String> {
         .next()
 }
 
-impl SyntaxErrors {
-    pub fn to_resolution(&self, code_fragment: &str) -> Vec<String> {
-        match self {
-            SyntaxErrors::InvalidFilter => vec!["Filter was invalid".to_string()],
-            SyntaxErrors::StartOfError => Vec::new(),
-            SyntaxErrors::NotAnOperator => {
-                let mut res = vec![format!("{} is not a valid operator", code_fragment)];
-                if let Some(choice) = did_you_mean(code_fragment, &VALID_OPERATORS) {
-                    res.push(format!("Did you mean \"{}\"?", choice));
-                }
-                res
-            }
-            SyntaxErrors::NotAnAggregateOperator => {
-                let mut res = vec![format!(
-                    "{} is not a valid aggregate operator",
-                    code_fragment
-                )];
-                if let Some(choice) = did_you_mean(code_fragment, &VALID_AGGREGATES) {
-                    res.push(format!("Did you mean \"{}\"?", choice));
-                }
-                if VALID_INLINE.contains(&code_fragment) {
-                    res.push(format!("{} is an inline operator, but only aggregate operators (count, average, etc.) are valid here", code_fragment))
-                }
-                res
-            }
-
-            SyntaxErrors::UnterminatedSingleQuotedString => {
-                vec!["Insert a single quote (') to terminate this string".to_string()]
-            }
-            SyntaxErrors::UnterminatedDoubleQuotedString => {
-                vec!["Insert a double quote (\") to terminate this string".to_string()]
-            }
-            SyntaxErrors::MissingParen => {
-                vec!["Insert a right parenthesis to terminate this expression".to_string()]
-            }
-            SyntaxErrors::MissingOperand => {
-                vec!["Add the operand or delete the operator".to_string()]
-            }
-            SyntaxErrors::MissingName => vec!["Give the value a name".to_string()],
-            SyntaxErrors::InvalidBooleanExpression => {
-                let mut base = vec![format!(
-                    "The boolean expression {} is invalid",
-                    code_fragment
-                )];
-                if code_fragment.contains("and") || code_fragment.contains("or") {
-                    base.push("AND and OR must be in UPPER CASE".to_string());
-                }
-                base
-            }
-        }
-    }
+/// Callback for handling error Snippets.
+pub trait ErrorReporter {
+    fn handle_error(&self, _snippet: Snippet) {}
 }
 
-/// Converts the ordinal from the nom error object back into a SyntaxError.
-impl From<u32> for SyntaxErrors {
-    fn from(ord: u32) -> Self {
-        // The FromPrimitive trait derived for this enum allows from_u32() to work.
-        SyntaxErrors::from_u32(ord).unwrap()
-    }
-}
+/// An ErrorReporter that writes errors related to the query string to the terminal
+pub struct TermErrorReporter {}
 
-impl From<SyntaxErrors> for ErrorKind {
-    fn from(e: SyntaxErrors) -> Self {
-        ErrorKind::Custom(e as u32)
-    }
-}
+impl ErrorReporter for TermErrorReporter {
+    fn handle_error(&self, mut snippet: Snippet) {
+        snippet.opt.color = env::var("NO_COLOR").is_err() && atty::is(Stream::Stderr);
+        let dl = annotate_snippets::display_list::DisplayList::from(snippet);
 
-/// Callback for handling error Snippets.
-pub trait ErrorReporter {
-    fn handle_error(&self, _snippet: Snippet) {}
+        eprintln!("{}", dl);
+    }
 }
 
 /// Container for data that will be used to construct a Snippet
@@ -233,21 +106,16 @@ impl<'a> SnippetBuilder<'a> {
     pub fn with_code_pointer<T, S: ToString>(mut self, pos: &Positioned<T>, label: S) -> Self {
         self.data
             .annotations
-            .push(((pos.start_pos.0, pos.end_pos.0), label.to_string()));
+            .push(((pos.range.start, pos.range.end), label.to_string()));
         self
     }
 
     /// Adds an annotation to a portion of the query string.  The given position will be
     /// highlighted with the accompanying label.
-    pub fn with_code_range<S: ToString>(
-        mut self,
-        start_pos: QueryPosition,
-        end_pos: QueryPosition,
-        label: S,
-    ) -> Self {
+    pub fn with_code_range<S: ToString>(mut self, range: Range<usize>, label: S) -> Self {
         self.data
             .annotations
-            .push(((start_pos.0, end_pos.0), label.to_string()));
+            .push(((range.start, range.end), label.to_string()));
         self
     }
 
@@ -257,32 +125,26 @@ impl<'a> SnippetBuilder<'a> {
         self
     }
 
-    /// Add a message to help the user resolve the error.
-    pub fn with_resolutions<T: IntoIterator<Item = String>>(mut self, resolutions: T) -> Self {
-        self.data.resolution.extend(resolutions.into_iter());
-        self
-    }
-
     /// Build and send the Snippet to the ErrorReporter in the QueryContainer.
-    pub fn send_report(mut self) {
+    pub fn send_report(self) {
         self.query.reporter.handle_error(Snippet {
             title: Some(Annotation {
-                label: Some(self.data.error),
+                label: Some(self.data.error.as_str()),
                 id: None,
                 annotation_type: AnnotationType::Error,
             }),
             slices: vec![Slice {
-                source: self.data.source,
+                source: self.data.source.as_str(),
                 line_start: 1,
                 origin: None,
                 fold: false,
                 annotations: self
                     .data
                     .annotations
-                    .drain(..)
-                    .map(move |anno| SourceAnnotation {
+                    .iter()
+                    .map(|anno| SourceAnnotation {
                         range: anno.0,
-                        label: anno.1,
+                        label: anno.1.as_str(),
                         annotation_type: AnnotationType::Error,
                     })
                     .collect(),
@@ -292,38 +154,12 @@ impl<'a> SnippetBuilder<'a> {
                 .resolution
                 .iter()
                 .map(|res| Annotation {
-                    label: Some(res.to_string()),
+                    label: Some(res),
                     id: None,
                     annotation_type: AnnotationType::Help,
                 })
                 .collect(),
+            opt: Default::default(),
         });
     }
 }
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn did_you_mean() {
-        assert_eq!(
-            SyntaxErrors::NotAnAggregateOperator.to_resolution("cont"),
-            vec![
-                "cont is not a valid aggregate operator",
-                "Did you mean \"count\"?"
-            ]
-        );
-        assert_eq!(
-            SyntaxErrors::NotAnOperator.to_resolution("cont"),
-            vec!["cont is not a valid operator", "Did you mean \"count\"?"]
-        );
-        assert_eq!(
-            SyntaxErrors::NotAnAggregateOperator.to_resolution("parse"),
-            vec![
-                "parse is not a valid aggregate operator",
-                "parse is an inline operator, but only aggregate operators (count, average, etc.) are valid here"
-            ]
-        );
-    }
-}
diff --git a/src/lang.rs b/src/lang.rs
index e2b206b..21f40ea 100644
--- a/src/lang.rs
+++ b/src/lang.rs
@@ -1,61 +1,33 @@
-use crate::alias::{self, AliasConfig};
-use crate::data;
-use crate::errors::SyntaxErrors;
+use std::fmt::Debug;
+use std::ops::Range;
+use std::str;
+
+use itertools::Itertools;
 use lazy_static::lazy_static;
-use nom;
-use nom::types::CompleteStr;
-use nom::*;
-use nom::{digit1, double, is_alphabetic, is_alphanumeric, is_digit, multispace};
+use nom::combinator::not;
+use nom::multi::fold_many0;
+use nom::sequence::{delimited, separated_pair};
+use nom::{
+    branch::alt,
+    bytes::complete::{escaped, take_while, take_while1},
+    character::complete::{anychar, digit1, multispace0, multispace1, none_of, satisfy},
+    character::{is_alphabetic, is_alphanumeric},
+    combinator::{eof, map, map_res, opt, peek, recognize},
+    error::ParseError,
+    multi::{many0, many_till, separated_list0, separated_list1},
+    number::complete::double,
+    sequence::{pair, tuple},
+    IResult, Parser, Slice,
+};
+use nom_locate::position;
 use nom_locate::LocatedSpan;
-use std::convert::From;
-use std::str;
+use nom_supreme::error::ErrorTree;
+use nom_supreme::parser_ext::ParserExt;
+use nom_supreme::tag::complete::tag;
 
-/// Wraps the result of the child parser in a Positioned and sets the start_pos and end_pos
-/// accordingly.
-macro_rules! with_pos {
-  ($i:expr, $submac:ident!( $($args:tt)* )) => ({
-      // XXX The ws!() combinator does not mix well with custom combinators since it does some
-      // rewriting, but only for things it knows about.  So, we put the ws!() combinator inside
-      // calls to with_pos!() and have with_pos!() eat up any initial space with space0().
-      match space0($i) {
-          Err(e) => Err(e),
-          Ok((i1, _o)) => {
-              let start_pos: QueryPosition = i1.into();
-              match $submac!(i1, $($args)*) {
-                  Ok((i, o)) => Ok((i, Positioned {
-                      start_pos,
-                      value: o,
-                      end_pos: i.into(),
-                  })),
-                  Err(e) => Err(e),
-              }
-          }
-      }
-  });
-  ($i:expr, $f:expr) => (
-    with_pos!($i, call!($f));
-  );
-}
-
-/// Dynamic version of `alt` that takes a slice of strings
-fn alternative<'a, T>(input: T, alternatives: &[&'a str]) -> IResult<T, T>
-where
-    T: InputTake,
-    T: Compare<&'a str>,
-    T: InputLength,
-    T: AtEof,
-    T: Clone,
-{
-    let mut last_err = None;
-    for alternative in alternatives {
-        let inp = input.clone();
-        match tag!(inp, alternative) {
-            done @ Ok(..) => return done,
-            err @ Err(..) => last_err = Some(err), // continue
-        }
-    }
-    last_err.unwrap()
-}
+use crate::alias::{self, AliasPipeline};
+use crate::data;
+use crate::errors::{ErrorBuilder, QueryContainer};
 
 pub const VALID_AGGREGATES: &[&str] = &[
     "count",
@@ -93,25 +65,20 @@ lazy_static! {
 
 pub const RESERVED_FILTER_WORDS: &[&str] = &["AND", "OR", "NOT"];
 
+#[derive(Debug)]
+struct Error(Range<usize>, String);
+
 /// Type used to track the current fragment being parsed and its location in the original input.
-pub type Span<'a> = LocatedSpan<CompleteStr<'a>>;
+pub type Span<'a> = LocatedSpan<&'a str, &'a QueryContainer>;
 
-/// Container for the position of some syntax in the input string.  This is similar to the Span,
-/// but it only contains the offset.
-#[derive(Debug, PartialEq, Clone)]
-pub struct QueryPosition(pub usize);
+pub type LResult<I, O, E = ErrorTree<I>> = Result<(I, O), nom::Err<E>>;
 
-impl<'a> From<Span<'a>> for QueryPosition {
-    fn from(located_span: Span<'a>) -> Self {
-        QueryPosition(located_span.offset)
-    }
-}
+pub type QueryRange = Range<usize>;
 
 /// Container for values from the query that records the location in the query string.
 #[derive(Debug, PartialEq, Clone)]
 pub struct Positioned<T> {
-    pub start_pos: QueryPosition,
-    pub end_pos: QueryPosition,
+    pub range: QueryRange,
     pub value: T,
 }
 
@@ -121,6 +88,58 @@ impl<T> Positioned<T> {
     }
 }
 
+/// Methods for converting a Span to different ranges over the span
+trait ToRange {
+    /// Return the entire Span as a Range
+    fn to_range(&self) -> Range<usize>;
+
+    /// Return a range from the current offset in the Span to the "sync point".  For the
+    /// angle-grinder syntax, the sync points are the vertical bar, end-of-query, and closing
+    /// braces/parens/etc...
+    fn to_sync_point(&self) -> Range<usize>;
+
+    /// Return a range from the current offset in the Span to the next whitespace
+    fn to_whitespace(&self) -> Range<usize>;
+}
+
+impl<'a> ToRange for Span<'a> {
+    fn to_range(&self) -> Range<usize> {
+        let start = self.location_offset();
+        let end = start + self.fragment().len();
+        start..end
+    }
+
+    fn to_sync_point(&self) -> Range<usize> {
+        let s: &str = self.fragment();
+        let sync = s.chars().find_position(|ch| match ch {
+            '|' | ')' | ']' | '}' => true,
+            _ => false,
+        });
+
+        let end = sync.map(|pair| pair.0).unwrap_or(s.len());
+
+        Range {
+            start: self.location_offset(),
+            end: self.location_offset() + end,
+        }
+    }
+
+    fn to_whitespace(&self) -> Range<usize> {
+        let s: &str = self.fragment();
+        let sync = s.chars().find_position(|ch| match ch {
+            ' ' | '\t' | '\n' => true,
+            _ => false,
+        });
+
+        let end = sync.map(|pair| pair.0).unwrap_or(s.len());
+
+        Range {
+            start: self.location_offset(),
+            end: self.location_offset() + end,
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, Clone)]
 pub enum ComparisonOp {
     Eq,
@@ -176,6 +195,7 @@ pub enum Expr {
         args: Vec<Expr>,
     },
     Value(data::Value),
+    Error,
 }
 
 impl Expr {
@@ -187,6 +207,147 @@ impl Expr {
     }
 }
 
+/// Debug helper
+pub fn dbg_dmp<'a, F, O, E>(
+    mut f: F,
+    context: &'static str,
+) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, O, E>
+where
+    F: FnMut(Span<'a>) -> IResult<Span<'a>, O, E>,
+{
+    move |i: Span<'a>| match f(i) {
+        Err(e) => {
+            println!("{}: Error at:\n{}", context, i);
+            Err(e)
+        }
+        Ok((s, a)) => {
+            println!("{}: Ok at:\n{}\nnext:\n{}", context, i, s.fragment());
+            Ok((s, a))
+        }
+    }
+}
+
+/// Combinator that expects the given parser to succeed.  If the parser returns an error:
+/// - the given error message is logged
+/// - the input is consumed up to the sync point
+/// - None is returned
+fn expect<'a, F, E, T>(
+    mut parser: F,
+    error_msg: E,
+) -> impl FnMut(Span<'a>) -> IResult<Span, Option<T>>
+where
+    F: FnMut(Span<'a>) -> IResult<Span, T>,
+    E: ToString,
+{
+    move |input: Span<'a>| match parser(input) {
+        Ok((remaining, out)) => Ok((remaining, Some(out))),
+        Err(nom::Err::Error(nom::error::Error { input, .. }))
+        | Err(nom::Err::Failure(nom::error::Error { input, .. })) => {
+            let r = input.to_sync_point();
+            let end = r.end - input.location_offset();
+            input
+                .extra
+                .report_error_for(error_msg.to_string())
+                .with_code_range(r, "")
+                .send_report();
+            Ok((input.slice(end..), None))
+        }
+        Err(err) => Err(err),
+    }
+}
+
+/// Combinator that expects the given parser to succeed.  If the parser returns an error:
+/// - the given error function is called with the range
+/// - the input is consumed up to the sync point
+/// - None is returned
+fn expect_fn<'a, F, O, EF>(
+    mut parser: F,
+    mut error_fn: EF,
+) -> impl FnMut(Span<'a>) -> IResult<Span, Option<O>>
+where
+    F: Parser<Span<'a>, O, nom::error::Error<Span<'a>>>,
+    EF: FnMut(&QueryContainer, QueryRange),
+{
+    move |input: Span<'a>| match parser.parse(input) {
+        Ok((remaining, out)) => Ok((remaining, Some(out))),
+        Err(nom::Err::Error(nom::error::Error { input, .. }))
+        | Err(nom::Err::Failure(nom::error::Error { input, .. })) => {
+            let r = input.to_sync_point();
+            let end = r.end - input.location_offset();
+            error_fn(input.extra, r);
+            let next = input.slice(end..);
+            Ok((next, None))
+        }
+        Err(err) => Err(err),
+    }
+}
+
+/// A version of the `delimited()` combinator() that calls an error-handling function when the
+/// terminator parser fails.
+pub fn expect_delimited<'a, O1, O2, O3, F, G, H, EF>(
+    mut first: F,
+    mut second: G,
+    mut third: H,
+    mut error_fn: EF,
+) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, O2, nom::error::Error<Span<'a>>>
+where
+    F: Parser<Span<'a>, O1, nom::error::Error<Span<'a>>>,
+    G: Parser<Span<'a>, O2, nom::error::Error<Span<'a>>>,
+    H: Parser<Span<'a>, O3, nom::error::Error<Span<'a>>>,
+    EF: FnMut(&QueryContainer, QueryRange),
+{
+    move |input: Span<'a>| {
+        let full_r = input.to_sync_point();
+        let (input, _) = first.parse(input)?;
+        let (input, o2) = second.parse(input)?;
+
+        match third.parse(input) {
+            Ok((input, _)) => Ok((input, o2)),
+            Err(_) => {
+                let start = input.location_offset();
+                let mut remaining = input;
+
+                loop {
+                    if remaining.is_empty() {
+                        error_fn(
+                            remaining.extra,
+                            Range {
+                                start: full_r.start,
+                                end: remaining.location_offset(),
+                            },
+                        );
+                        return Ok((remaining, o2));
+                    }
+
+                    remaining = remaining.slice(1..);
+                    let end = remaining.location_offset();
+                    let res = third.parse(remaining);
+
+                    if let Ok((remaining, _)) = res {
+                        remaining
+                            .extra
+                            .report_error_for("unhandled input")
+                            .with_code_range(Range { start, end }, "")
+                            .send_report();
+                        return Ok((remaining, o2));
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Combinator that expects some optional whitespace followed by the vertical bar or EOF.  If that
+/// input is not found, the error message is logged.
+fn expect_pipe<'a, M>(
+    error_msg: M,
+) -> impl FnMut(Span<'a>) -> IResult<Span, Option<(Span<'a>, Span<'a>)>>
+where
+    M: ToString,
+{
+    expect(peek(multispace0.and(tag("|").or(eof))), error_msg)
+}
+
 /// The KeywordType determines how a keyword string should be interpreted.
 #[derive(Debug, PartialEq, Eq, Clone)]
 enum KeywordType {
@@ -233,7 +394,7 @@ impl Keyword {
     }
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum Search {
     And(Vec<Search>),
     Or(Vec<Search>),
@@ -242,20 +403,32 @@ pub enum Search {
 }
 
 impl Search {
-    fn no_op(&self) -> bool {
-        match self {
-            Search::Keyword(Keyword(k, _)) => k.is_empty(),
-            _ => false,
+    pub fn from_quoted_input(s: String) -> Option<Self> {
+        if s.is_empty() {
+            None
+        } else {
+            Some(Search::Keyword(Keyword::new_exact(s)))
+        }
+    }
+
+    pub fn from_keyword_input(s: &str) -> Option<Self> {
+        let trimmed = s.trim_matches('*');
+
+        if trimmed.is_empty() {
+            None
+        } else {
+            Some(Search::Keyword(Keyword::new_wildcard(trimmed.to_string())))
         }
     }
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum Operator {
-    RenderedAlias(Positioned<String>),
+    RenderedAlias(Vec<Operator>),
     Inline(Positioned<InlineOperator>),
     MultiAggregate(MultiAggregateOperator),
     Sort(SortOperator),
+    Error,
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -310,13 +483,13 @@ pub enum FieldMode {
     Except,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum SortMode {
     Ascending,
     Descending,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub enum AggregateFunction {
     Count {
         condition: Option<Expr>,
@@ -343,25 +516,243 @@ pub enum AggregateFunction {
     },
 }
 
-#[derive(Debug, PartialEq)]
+impl AggregateFunction {
+    fn default_name(&self) -> String {
+        match self {
+            AggregateFunction::Count { .. } => "_count".to_string(),
+            AggregateFunction::Sum { .. } => "_sum".to_string(),
+            AggregateFunction::Min { .. } => "_min".to_string(),
+            AggregateFunction::Average { .. } => "_average".to_string(),
+            AggregateFunction::Max { .. } => "_max".to_string(),
+            AggregateFunction::Percentile { .. } => "p".to_string(),
+            AggregateFunction::CountDistinct { .. } => "_countDistinct".to_string(),
+        }
+    }
+}
+
+#[derive(Debug, PartialEq, Clone)]
 pub struct MultiAggregateOperator {
     pub key_cols: Vec<Expr>,
     pub key_col_headers: Vec<String>,
     pub aggregate_functions: Vec<(String, Positioned<AggregateFunction>)>,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub struct SortOperator {
     pub sort_cols: Vec<Expr>,
     pub direction: SortMode,
 }
 
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Clone)]
 pub struct Query {
     pub search: Search,
     pub operators: Vec<Operator>,
 }
 
+/// Parses the +/- binary operators
+fn addsub_op(input: Span) -> IResult<Span, ArithmeticOp> {
+    alt((
+        tag("+").map(|_| ArithmeticOp::Add),
+        tag("-").map(|_| ArithmeticOp::Subtract),
+    ))(input)
+}
+
+/// Parses an argument list for a function
+fn arg_list(input: Span) -> IResult<Span, Vec<Expr>> {
+    expect_delimited(
+        tag("(").and(multispace0),
+        separated_list0(tag(","), delimited(multispace0, opt_expr, multispace0)),
+        tag(")"),
+        |qc, r| {
+            qc.report_error_for("unterminated function call")
+                .with_code_range(r, "unterminated function call")
+                .with_resolution("Insert a right parenthesis to terminate this call")
+                .send_report()
+        },
+    )
+    .parse(input)
+}
+
+/// Parses a, potentially optional, argument list for a function that has a single parameter
+fn single_arg(description: &'static str) -> impl Clone + Fn(Span) -> IResult<Span, Expr> {
+    move |input: Span| {
+        expect_delimited(
+            tag("(").and(multispace0),
+            expect_fn(opt_expr, |qc, r| {
+                qc.report_error_for("this operator takes 1 argument, but 0 were supplied")
+                    .with_code_range(r, "- supplied 0 arguments")
+                    .with_resolution(format!("the argument should supply {}", description))
+                    .send_report();
+            })
+            .map(|e| e.unwrap_or(Expr::Error)),
+            tag(")"),
+            |qc, r| {
+                qc.report_error_for("unterminated function call")
+                    .with_code_range(r, "unterminated function call")
+                    .with_resolution("Insert a right parenthesis to terminate this call")
+                    .send_report()
+            },
+        )
+        .parse(input)
+    }
+}
+
+/// A version of the single_arg parser that logs an error if no argument list was provided
+fn req_single_arg(description: &'static str) -> impl Clone + Fn(Span) -> IResult<Span, Expr> {
+    move |input: Span| {
+        expect_fn(single_arg(description), |qc, r| {
+            qc.report_error_for(format!(
+                "expecting a parenthesized argument that supplies {}",
+                description
+            ))
+            .with_code_range(r, "")
+            .send_report()
+        })
+        .map(|e| e.unwrap_or(Expr::Error))
+        .parse(input)
+    }
+}
+
+/// Combinator that checks for an optional keyword that should be followed by an expression
+fn kw_expr(
+    keyword: &'static str,
+    description: &'static str,
+) -> impl Fn(Span) -> IResult<Span, Option<Expr>> {
+    move |input: Span| {
+        let res = opt(tag(keyword).preceded_by(multispace1)).parse(input)?;
+
+        match res {
+            (input, None) => Ok((input, None)),
+            (input, Some(keyword_span)) => {
+                expect_fn(opt_expr.preceded_by(multispace1), |qc, _r| {
+                    qc.report_error_for(format!(
+                        "expecting an expression that supplies {}",
+                        description
+                    ))
+                    .with_code_range(
+                        keyword_span.to_range(),
+                        "should be followed by an expression",
+                    )
+                    .send_report();
+                })
+                .map(|e| e.map(Some).unwrap_or(Some(Expr::Error)))
+                .parse(input)
+            }
+        }
+    }
+}
+
+fn fcall(input: Span) -> IResult<Span, Expr> {
+    ident
+        .and(arg_list)
+        .map(|(name, args)| Expr::FunctionCall { name, args })
+        .parse(input)
+}
+
+fn filter_atom(input: Span) -> IResult<Span, Option<Search>> {
+    let keyword = take_while1(is_keyword).map(|i: Span| Search::from_keyword_input(i.fragment()));
+
+    alt((quoted_string.map(Search::from_quoted_input), keyword))(input)
+}
+
+fn filter_not(input: Span) -> IResult<Span, Option<Search>> {
+    tag("NOT")
+        .precedes(multispace1)
+        .precedes(low_filter)
+        .map(|optk| optk.map(|k| Search::Not(Box::new(k))))
+        .parse(input)
+}
+
+fn sourced_expr(input: Span) -> IResult<Span, (String, Expr)> {
+    recognize(expr)
+        .map(|ex| (ex.fragment().trim().to_string(), expr(ex).ok().unwrap().1))
+        .parse(input)
+}
+
+fn sourced_expr_list(input: Span) -> IResult<Span, Vec<(String, Expr)>> {
+    separated_list1(tag(",").delimited_by(multispace0), sourced_expr)(input)
+}
+
+fn sort_mode(input: Span) -> IResult<Span, SortMode> {
+    alt((
+        alt((tag("asc"), tag("ascending"))).map(|_| SortMode::Ascending),
+        alt((tag("desc"), tag("dsc"), tag("descending"))).map(|_| SortMode::Descending),
+    ))(input)
+}
+
+fn sort(input: Span) -> IResult<Span, Operator> {
+    tuple((
+        tag("sort").precedes(
+            opt(tag("by")
+                .delimited_by(multispace1)
+                .precedes(sourced_expr_list))
+            .map(|opt_cols| opt_cols.unwrap_or_else(Vec::new)),
+        ),
+        opt(sort_mode.preceded_by(multispace1))
+            .map(|opt_mode| opt_mode.unwrap_or(SortMode::Ascending)),
+    ))
+    .map(|(mut sort_cols, direction)| {
+        Operator::Sort(SortOperator {
+            sort_cols: sort_cols.drain(..).map(|(_name, ex)| ex).collect(),
+            direction,
+        })
+    })
+    .parse(input)
+}
+
+fn filter_explicit_and(input: Span) -> IResult<Span, Option<Search>> {
+    separated_pair(low_filter, tag("AND").delimited_by(multispace1), low_filter)
+        .map(|p| match p {
+            (Some(l), Some(r)) => Some(Search::And(vec![l, r])),
+            (Some(l), None) => Some(l),
+            (None, Some(r)) => Some(r),
+            (None, None) => None,
+        })
+        .parse(input)
+}
+
+fn filter_explicit_or(input: Span) -> IResult<Span, Option<Search>> {
+    separated_pair(mid_filter, tag("OR").delimited_by(multispace1), mid_filter)
+        .map(|p| match p {
+            (Some(l), Some(r)) => Some(Search::Or(vec![l, r])),
+            (Some(l), None) => Some(l),
+            (None, Some(r)) => Some(r),
+            (None, None) => None,
+        })
+        .parse(input)
+}
+
+fn low_filter(input: Span) -> IResult<Span, Option<Search>> {
+    alt((
+        filter_not,
+        filter_atom,
+        expect_delimited(tag("("), high_filter, tag(")"), |qc, r| {
+            qc.report_error_for("unterminated parenthesized filter")
+                .with_code_range(r, "unterminated parenthesized filter")
+                .with_resolution("Insert a right parenthesis to terminate this filter")
+                .send_report()
+        }),
+    ))(input)
+}
+
+fn mid_filter(input: Span) -> IResult<Span, Option<Search>> {
+    alt((filter_explicit_and, low_filter))(input)
+}
+
+fn high_filter(input: Span) -> IResult<Span, Option<Search>> {
+    alt((filter_explicit_or, mid_filter))(input)
+}
+
+fn end_of_query(input: Span) -> IResult<Span, Span> {
+    peek(multispace0.precedes(alt((peek(tag("|")), eof))))(input)
+}
+
+fn parse_search(input: Span) -> IResult<Span, Search> {
+    many_till(high_filter.delimited_by(multispace0), end_of_query)
+        .map(|(s, _)| Search::And(s.into_iter().flatten().collect()))
+        .parse(input)
+}
+
 fn is_ident(c: char) -> bool {
     is_alphanumeric(c as u8) || c == '_'
 }
@@ -383,1255 +774,1745 @@ fn is_keyword(c: char) -> bool {
     }
 }
 
-fn not_escape_sq(c: char) -> bool {
-    c != '\\' && c != '\''
+fn duration(input: Span) -> IResult<Span, chrono::Duration> {
+    let (input, amount) = i64_parse(input)?;
+
+    let retval = alt((
+        tag("s").map(|_| chrono::Duration::seconds(amount)),
+        tag("m").map(|_| chrono::Duration::minutes(amount)),
+        tag("h").map(|_| chrono::Duration::hours(amount)),
+        tag("d").map(|_| chrono::Duration::days(amount)),
+        tag("w").map(|_| chrono::Duration::weeks(amount)),
+    ))
+    .parse(input);
+
+    retval
 }
 
-fn not_escape_dq(c: char) -> bool {
-    c != '\\' && c != '\"'
+fn dot_property(input: Span) -> IResult<Span, DataAccessAtom> {
+    tag(".")
+        .precedes(ident)
+        .map(DataAccessAtom::Key)
+        .parse(input)
 }
-named!(value<Span, data::Value>, ws!(
-    alt!(
-        map!(quoted_string, |s|data::Value::Str(s.to_string()))
-        | map!(digit1, |s|data::Value::from_string(s.fragment.0))
-    )
-));
-
-named!(duration<Span, chrono::Duration>, ws!(do_parse!(
-    amount: map!(digit1, |s| s.fragment.0.parse::<i64>().unwrap()) >>
-    value: alt!(
-        map!(tag!("s"), |_| chrono::Duration::seconds(amount)) |
-        map!(tag!("m"), |_| chrono::Duration::minutes(amount)) |
-        map!(tag!("h"), |_| chrono::Duration::hours(amount)) |
-        map!(tag!("d"), |_| chrono::Duration::days(amount)) |
-        map!(tag!("w"), |_| chrono::Duration::weeks(amount))
-    ) >>
-    (value)
-)));
-
-named!(dot_property<Span, DataAccessAtom>, do_parse!(
-    field: preceded!(tag!("."), ident) >>
-    (DataAccessAtom::Key(field))
-));
-
-named!(i64_parser<Span, i64>,
-    map_res!(
-        recognize!(tuple!(opt!(char!('-')), digit)),
-        |s: Span|s.fragment.0.parse::<i64>())
-);
-
-named!(index_access<Span, DataAccessAtom>, do_parse!(
-    index: delimited!(tag!("["), i64_parser, tag!("]"))  >>
-    (DataAccessAtom::Index(index))
-));
-
-named!(column_ref<Span, Expr>, do_parse!(
-    head: ident >>
-    rest: many0!(alt!(dot_property | index_access)) >>
-    (Expr::Column { head: DataAccessAtom::Key(head), rest: rest })
-));
-
-named!(ident<Span, String>, alt!(bare_ident | escaped_ident));
-
-named!(bare_ident<Span, String>, do_parse!(
-    start: take_while1!(starts_ident) >>
-    rest: take_while!(is_ident) >>
-    (start.fragment.0.to_owned() + rest.fragment.0)
-));
-
-named!(escaped_ident<Span, String>,
-    delimited!(tag!("["), map!(quoted_string, |s| s.to_owned()), tag!("]"))
-);
-
-named!(arguments<Span, Vec<Expr>>, add_return_error!(SyntaxErrors::StartOfError.into(), delimited!(
-   tag!("("),
-   separated_list!(tag!(","), expr),
-   return_error!(SyntaxErrors::MissingParen.into(), tag!(")"))
-)));
-
-named!(e_ident<Span, Expr>,
-    ws!(alt_complete!(
-      map!(pair!(ident, arguments), |(name, args)| Expr::FunctionCall { name, args })
-    | column_ref
-    | map!(value, Expr::Value)
-    | ws!(add_return_error!(SyntaxErrors::StartOfError.into(), delimited!(
-          tag!("("),
-          expr,
-          return_error!(SyntaxErrors::MissingParen.into(), tag!(")")))))
-)));
-
-named!(unary<Span, Expr>, ws!(do_parse!(
-   opt_op: opt!(unary_op) >>
-   atomic: e_ident >>
-   (if let Some(op) = opt_op {
-       Expr::Unary {
-          op: op,
-          operand: Box::new(atomic)
-       }
-    } else {
-       atomic
-    })
-)));
-
-named!(raw_keyword<Span, Span>,
-   take_while1!(is_keyword)
-);
-
-named!(keyword<Span, String>, map!(
-    verify!(raw_keyword, |k: Span|!RESERVED_FILTER_WORDS.contains(&k.fragment.0)),
-    |span|span.fragment.0.to_owned()
-
-));
-
-named!(comp_op<Span, ComparisonOp>, ws!(alt_complete!(
-    map!(tag!("=="), |_|ComparisonOp::Eq)
-    | map!(tag!("<="), |_|ComparisonOp::Lte)
-    | map!(tag!(">="), |_|ComparisonOp::Gte)
-    | map!(tag!("!="), |_|ComparisonOp::Neq)
-    | map!(tag!(">"), |_|ComparisonOp::Gt)
-    | map!(tag!("<"), |_|ComparisonOp::Lt)
-)));
-
-named!(unary_op<Span, UnaryOp>, ws!(alt_complete!(
-    map!(tag!("!"), |_|UnaryOp::Not)
-)));
-
-named!(term<Span, Expr>, do_parse!(
-   init: unary >>
-   res: fold_many0!(
-      add_return_error!(SyntaxErrors::StartOfError.into(), pair!(ws!(alt!(
-         map!(tag!("*"), |_|ArithmeticOp::Multiply)
-         | map!(tag!("/"), |_|ArithmeticOp::Divide)
-      )), return_error!(SyntaxErrors::MissingOperand.into(), unary))),
-      init,
-      |left, (op, right)| {
-          Expr::Binary {
-             left: Box::new(left),
-             op: BinaryOp::Arithmetic(op),
-             right: Box::new(right)
-          }
-      }
-   ) >>
-   (res)
-));
-
-named!(arith_expr<Span, Expr>, ws!(do_parse!(
-   init: term >>
-   res: fold_many0!(
-      add_return_error!(SyntaxErrors::StartOfError.into(), pair!(ws!(alt!(
-         map!(tag!("+"), |_|ArithmeticOp::Add)
-         | map!(tag!("-"), |_|ArithmeticOp::Subtract)
-      )), return_error!(SyntaxErrors::MissingOperand.into(), term))),
-      init,
-      |left, (op, right)| {
-         Expr::Binary {
-             left: Box::new(left),
-             op: BinaryOp::Arithmetic(op),
-             right: Box::new(right)
-         }
-      }
-   ) >>
-   (res)
-)));
-
-named!(expr<Span, Expr>, ws!(do_parse!(
-   init: arith_expr >>
-   res: opt!(pair!(comp_op, arith_expr)) >>
-   (if let Some((op, right)) = res {
-       Expr::Binary {
-          left: Box::new(init),
-          op: BinaryOp::Comparison(op),
-          right: Box::new(right)
-       }
-    } else {
-       init
-    })
-)));
-
-named!(req_ident<Span, String>, return_error!(SyntaxErrors::MissingName.into(), ident));
-
-named!(field_expr<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-   not!(alt_complete!(
-       tag!("count") |
-       tag!("count_distinct") |
-       tag!("total")
-   )) >>
-   value: expr >>
-   tag!("as") >>
-   name: req_ident >>
-   (InlineOperator::FieldExpression {
-       value,
-       name,
-    })
-))));
-
-named!(json<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("json") >>
-    from_column_opt: opt!(ws!(preceded!(tag!("from"), expr))) >>
-    (InlineOperator::Json { input_column: from_column_opt })
-))));
-
-named!(logfmt<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("logfmt") >>
-    from_column_opt: opt!(ws!(preceded!(tag!("from"), expr))) >>
-    (InlineOperator::Logfmt { input_column: from_column_opt })
-))));
-
-named!(whre<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("where") >>
-    ex: opt!(with_pos!(expr)) >>
-    (InlineOperator::Where { expr: ex })
-))));
-
-named!(limit<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("limit") >>
-    count: opt!(with_pos!(double)) >>
-    (InlineOperator::Limit{
-        count
-    })
-))));
-
-named!(timeslice<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("timeslice") >>
-    input_column: delimited!(tag!("("), expr, tag!(")")) >>
-    duration: opt!(duration) >>
-    output_column: opt!(ws!(preceded!(tag!("as"), ident))) >>
-    (InlineOperator::Timeslice {
-        input_column,
-        duration,
-        output_column
+
+fn i64_parse(input: Span) -> IResult<Span, i64> {
+    map_res(recognize(opt(tag("-")).precedes(digit1)), |s: Span| {
+        s.fragment().parse::<i64>()
     })
-))));
-
-named!(split<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("split") >>
-    from_column_opt: opt!(delimited!(
-        tag!("("),
-        expr,
-        tag!(")")
-    )) >>
-    separator_opt: opt!(ws!(preceded!(tag!("on"), quoted_string))) >>
-    // TODO: make variant of expr that only accepts Expr::Column instead of all types
-    rename_opt: opt!(ws!(preceded!(tag!("as"), expr))) >>
-    (InlineOperator::Split {
-        separator: separator_opt.map(|s| s.to_string()).unwrap_or_else(|| ",".to_string()),
-        input_column: from_column_opt.clone(),
-        // If from column specified, but output column not specified
-        // output should be the from column.
-        output_column: rename_opt.or(from_column_opt),
+    .parse(input)
+}
+
+fn index_access(input: Span) -> IResult<Span, DataAccessAtom> {
+    delimited(tag("["), i64_parse, tag("]"))
+        .map(DataAccessAtom::Index)
+        .parse(input)
+}
+
+fn column_ref(input: Span) -> IResult<Span, Expr> {
+    tuple((ident, many0(alt((dot_property, index_access)))))
+        .map(|(head, rest)| Expr::Column {
+            head: DataAccessAtom::Key(head),
+            rest,
+        })
+        .parse(input)
+}
+
+fn ident(input: Span) -> IResult<Span, String> {
+    alt((bare_ident, escaped_ident))(input)
+}
+
+fn bare_ident(input: Span) -> IResult<Span, String> {
+    recognize(pair(satisfy(starts_ident), take_while(is_ident)))
+        .map(|span: Span| span.fragment().to_string())
+        .parse(input)
+}
+
+fn escaped_ident(input: Span) -> IResult<Span, String> {
+    expect_delimited(tag("["), quoted_string, tag("]"), |qc, r| {
+        qc.report_error_for("unterminated identifier")
+            .with_code_range(r, "")
+            .with_resolution("Insert a closing square bracket")
+            .send_report()
     })
-))));
-
-named!(total<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("total") >>
-    input_column: delimited!(tag!("("), expr, tag!(")")) >>
-    rename_opt: opt!(ws!(preceded!(tag!("as"), ident))) >>
-    (InlineOperator::Total{
-        input_column,
-        output_column:
-            rename_opt.unwrap_or_else(||"_total".to_string()),
-})))));
-
-named!(double_quoted_string <Span, &str>, add_return_error!(
-    SyntaxErrors::StartOfError.into(), delimited!(
-        tag!("\""),
-        map!(escaped!(take_while1!(not_escape_dq), '\\', anychar), |ref s|s.fragment.0),
-        return_error!(SyntaxErrors::UnterminatedDoubleQuotedString.into(), tag!("\""))
-)));
-
-named!(single_quoted_string <Span, &str>, add_return_error!(
-    SyntaxErrors::StartOfError.into(), delimited!(
-        tag!("'"),
-        map!(escaped!(take_while1!(not_escape_sq), '\\', anychar), |ref s|s.fragment.0),
-        return_error!(SyntaxErrors::UnterminatedSingleQuotedString.into(), tag!("'"))
-)));
-
-named!(quoted_string<Span, &str>, alt_complete!(double_quoted_string | single_quoted_string));
-
-named!(var_list<Span, Vec<String> >, ws!(separated_nonempty_list!(
-    tag!(","), ws!(ident)
-)));
-
-named!(sourced_expr_list<Span, Vec<(String, Expr)> >, ws!(separated_nonempty_list!(
-    tag!(","), ws!(sourced_expr)
-)));
-
-named!(sourced_expr<Span, (String, Expr)>, ws!(
-    do_parse!(
-        ex: recognize!(expr) >>
-        (
-            (ex.fragment.0.trim().to_string(), expr(ex).unwrap().1)
-        )
-)));
-
-named_args!(did_you_mean<'a>(choices: &[&'a str], err: SyntaxErrors)<Span<'a>, Span<'a>>,
-        preceded!(space0,
-            return_error!(SyntaxErrors::StartOfError.into(),
-                alt_complete!(
-                    // Either we find a valid operator name
-                    // To prevent a prefix from partially matching, require `not!(alpha1)` after the tag is consumed
-                    terminated!(alt_complete!(pct_fn | call!(alternative, choices)), not!(alpha1)) |
-
-                    // Or we return an error after consuming a word
-                    // If we exhaust all other possibilities, consume a word and return an error. We won't
-                    // find `tag!("a")` after an identifier, so that's a guaranteed to fail and produce `not an operator`
-                    terminated!(take_while!(is_ident), return_error!(err.clone().into(), tag!("a")))))
-        )
-);
-
-named!(did_you_mean_operator<Span, Span>,
-    call!(did_you_mean, &VALID_OPERATORS, SyntaxErrors::NotAnOperator)
-);
-
-named!(did_you_mean_aggregate<Span, Span>,
-    call!(did_you_mean, &VALID_AGGREGATES, SyntaxErrors::NotAnAggregateOperator)
-);
-
-// parse "blah * ... *" [from other_field] as x, y
-named!(parse<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("parse") >>
-    pattern: quoted_string >>
-    from_column_opt: opt!(with_pos!(ws!(preceded!(tag!("from"), expr)))) >>
-    tag!("as") >>
-    vars: var_list >>
-    from_column_opt_2: opt!(with_pos!(ws!(preceded!(tag!("from"), expr)))) >>
-    no_drop_opt: opt!(ws!(tag!("nodrop"))) >>
-    ( InlineOperator::Parse{
-        pattern: Keyword::new_wildcard(pattern.to_string()),
-        fields: vars,
-        input_column: (from_column_opt, from_column_opt_2),
-        no_drop: no_drop_opt.is_some()
-        } )
-))));
-
-named!(fields_mode<Span, FieldMode>, alt_complete!(
-    map!(
-        alt_complete!(tag!("+") | tag!("only") | tag!("include")),
-        |_|FieldMode::Only
-    ) |
-    map!(
-        alt_complete!(tag!("-") | tag!("except") | tag!("drop")),
-        |_|FieldMode::Except
-    )
-));
-
-named!(fields<Span, Positioned<InlineOperator>>, with_pos!(ws!(do_parse!(
-    tag!("fields") >>
-    mode: opt!(fields_mode) >>
-    fields: var_list >>
-    (
-        InlineOperator::Fields {
-            mode: mode.unwrap_or(FieldMode::Only),
-            fields
-        }
-    )
-))));
-
-named!(arg_list<Span, Positioned<Vec<Expr>>>, add_return_error!(
-    SyntaxErrors::StartOfError.into(), with_pos!(delimited!(
-        tag!("("),
-        ws!(separated_list!(tag!(","), ws!(expr))),
-        return_error!(SyntaxErrors::MissingParen.into(), tag!(")"))))
-));
-
-named!(count<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    tag!("count") >>
-    condition: opt!(delimited!(tag!("("), expr ,tag!(")"))) >>
-    (AggregateFunction::Count { condition })
-))));
-
-named!(min<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    tag!("min") >>
-    column: delimited!(tag!("("), expr,tag!(")")) >>
-    (AggregateFunction::Min{column})
-))));
-
-named!(average<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    alt_complete!(tag!("avg") | tag!("average")) >>
-    column: delimited!(tag!("("), expr ,tag!(")")) >>
-    (AggregateFunction::Average{column})
-))));
-
-named!(max<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    tag!("max") >>
-    column: delimited!(tag!("("), expr,tag!(")")) >>
-    (AggregateFunction::Max{column})
-))));
-
-named!(count_distinct<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    tag!("count_distinct") >>
-    column: opt!(arg_list) >>
-    (AggregateFunction::CountDistinct{ column })
-))));
-
-named!(sum<Span, Positioned<AggregateFunction>>, with_pos!(ws!(do_parse!(
-    tag!("sum") >>
-    column: delimited!(tag!("("), expr,tag!(")")) >>
-    (AggregateFunction::Sum{column})
-))));
-
-fn is_digit_char(digit: char) -> bool {
-    is_digit(digit as u8)
-}
-
-named!(pct_fn<Span, Span>, preceded!(
-    alt_complete!(tag!("pct") | tag!("percentile") | tag!("p")),
-    take_while_m_n!(2, 2, is_digit_char)
-));
-
-named!(p_nn<Span, Positioned<AggregateFunction>>, ws!(
-    with_pos!(do_parse!(
-        pct: pct_fn >>
-        column: delimited!(tag!("("), expr,tag!(")")) >>
-        (AggregateFunction::Percentile{
-            column,
-            percentile: (".".to_owned() + pct.fragment.0).parse::<f64>().unwrap(),
-            percentile_str: pct.fragment.0.to_string()
+    .parse(input)
+}
+
+/// Parses the basic unit of an expression
+fn atomic(input: Span) -> IResult<Span, Expr> {
+    let num = digit1.map(|s: Span| data::Value::from_string(*s.fragment()));
+    let quoted_string_value = quoted_string.map(data::Value::Str);
+    let value = alt((quoted_string_value, num)).map(Expr::Value);
+    let parens = expect_delimited(tag("("), expr, tag(")"), |qc, r| {
+        qc.report_error_for("unterminated parenthesized expression")
+            .with_code_range(r, "unterminated parenthesized expression")
+            .with_resolution("Insert a right parenthesis to terminate this expression")
+            .send_report()
+    });
+
+    alt((fcall, column_ref, value, parens)).parse(input)
+}
+
+/// Parses an atomic expression with an optional unary prefix
+fn unary(input: Span) -> IResult<Span, Expr> {
+    let (input, opt_op) = opt(unary_op)(input)?;
+
+    match opt_op {
+        None => atomic(input),
+        Some(op) => expect_fn(atomic, |qc, r| {
+            qc.report_error_for("expecting expression for unary operator")
+                .with_code_range(r, "-")
+                .send_report()
         })
-    ))
-));
-
-fn string_from_located_span(
-    span: LocatedSpan<impl nom::AsBytes>,
-) -> Result<String, std::string::FromUtf8Error> {
-    String::from_utf8(span.fragment.as_bytes().to_vec())
-}
-
-named!(alias<Span, Operator>, map!(
-    with_pos!(ws!(do_parse!(
-        config: map_res!(map_res!(nom::alpha, string_from_located_span), AliasConfig::matching_string) >>
-        (config.render())
-    ))),
-    Operator::RenderedAlias
-));
-
-named!(inline_operator<Span, Operator>, do_parse!(
-    peek!(did_you_mean_operator) >>
-    res: map!(alt_complete!(parse | json | logfmt | fields | whre | limit | total | split | timeslice), Operator::Inline) >>
-    (res)
-));
-
-named!(aggregate_function<Span, Positioned<AggregateFunction>>, do_parse!(
-    peek!(did_you_mean_aggregate) >>
-    res: alt_complete!(
-        count_distinct |
-        count |
-        min |
-        average |
-        max |
-        sum |
-        p_nn) >> (res)
-));
-
-named!(operator<Span, Operator>, alt_complete!(
-    map!(field_expr, Operator::Inline) |
-    inline_operator | sort | alias | multi_aggregate_operator)
-);
-
-// count by x,y
-// avg(foo) by x
-
-fn default_output(func: &Positioned<AggregateFunction>) -> String {
-    match func.into() {
-        AggregateFunction::Count { .. } => "_count".to_string(),
-        AggregateFunction::Sum { .. } => "_sum".to_string(),
-        AggregateFunction::Min { .. } => "_min".to_string(),
-        AggregateFunction::Average { .. } => "_average".to_string(),
-        AggregateFunction::Max { .. } => "_max".to_string(),
-        AggregateFunction::CountDistinct { .. } => "_countDistinct".to_string(),
-        AggregateFunction::Percentile {
-            ref percentile_str, ..
-        } => "p".to_string() + percentile_str,
+        .map(|operand| Expr::Unary {
+            op: op.clone(),
+            operand: Box::new(operand.unwrap_or(Expr::Error)),
+        })
+        .parse(input),
     }
 }
 
-named!(complete_agg_function<Span, (String, Positioned<AggregateFunction>)>, ws!(do_parse!(
-        agg_function: aggregate_function >>
-        rename_opt: opt!(ws!(preceded!(tag!("as"), ident))) >>
-        (
-            rename_opt.unwrap_or_else(||default_output(&agg_function)),
-            agg_function
-        )
-    ))
-);
-
-named!(multi_aggregate_operator<Span, Operator>, ws!(do_parse!(
-    agg_functions: ws!(separated_nonempty_list!(tag!(","), complete_agg_function)) >>
-    key_cols_opt: opt!(preceded!(tag!("by"), sourced_expr_list)) >>
-    (Operator::MultiAggregate(MultiAggregateOperator {
-        key_col_headers: key_cols_opt.clone()
-            .unwrap_or_default()
-            .iter().cloned().map(|col|col.0).collect(),
-        key_cols: key_cols_opt
-            .unwrap_or_default()
-            .iter().cloned().map(|col|col.1).collect(),
-        aggregate_functions: agg_functions,
-     })))
-));
-
-named!(sort_mode<Span, SortMode>, alt_complete!(
-    map!(
-        alt_complete!(tag!("asc") | tag!("ascending")),
-        |_|SortMode::Ascending
-    ) |
-    map!(
-        alt_complete!(tag!("desc") | tag!("dsc") | tag!("descending")),
-        |_|SortMode::Descending
-    )
-));
-
-named!(sort<Span, Operator>, ws!(do_parse!(
-    tag!("sort") >>
-    key_cols_opt: opt!(preceded!(opt!(tag!("by")), sourced_expr_list)) >>
-    dir: opt!(sort_mode) >>
-    (Operator::Sort(SortOperator{
-        sort_cols: key_cols_opt.unwrap_or_default().into_iter().map(|(_source,expr)|expr).collect(),
-        direction: dir.unwrap_or(SortMode::Ascending) ,
-     })))
-));
-
-named!(filter_explicit_and<Span, Search>, do_parse!(
-    peek!(ws!(pair!(low_filter, alt!(tag!("AND") | tag!("and"))))) >>
-    res: map!(
-        return_error!(SyntaxErrors::InvalidBooleanExpression.into(),
-            separated_pair!(ws!(low_filter),
-            ws!(tag!("AND")),
-            ws!(low_filter))), |(l,r)|Search::And(vec![l, r])
-    ) >> (res)
-));
-
-named!(filter_explicit_or<Span, Search>, do_parse!(
-    peek!(ws!(pair!(low_filter, alt!(tag!("OR") | tag!("or"))))) >>
-    res: map!(
-        return_error!(SyntaxErrors::InvalidBooleanExpression.into(),
-            separated_pair!(ws!(mid_filter),
-            ws!(tag!("OR")),
-            ws!(mid_filter))), |(l,r)|Search::Or(vec![l, r])
-    ) >> (res)
-));
-
-// Top level:
-// Look for OR
-// Look for AND
-// Implicit AND
-
-/* A list of things is implicitly ANDed together */
-named!(filter_implicit_and<Span, Search>, map!(
-    separated_nonempty_list!(multispace, low_filter),
-    // An empty keyword would match everything, so there's no reason to
-    |mut v| {
-        v.retain( | k | ! k.no_op());
-        Search::And(v)
-    }
-));
-
-named!(filter_atom<Span, Search>, alt_complete!(
-    map!(quoted_string, |s| Search::Keyword(Keyword::new_exact(s.to_string()))) |
-    map!(keyword, |s| Search::Keyword(Keyword::new_wildcard(s.trim_matches('*').to_string())))
-));
-
-named!(high_filter<Span, Search>, alt_complete!(
-  filter_explicit_or |
-  mid_filter
-));
-
-named!(mid_filter<Span, Search>, alt_complete!(
-  filter_explicit_and |
-  low_filter
-));
-
-named!(low_filter<Span, Search>, alt_complete!(
-  filter_not
-  | filter_atom
-  | delimited!(ws!(tag!("(")), high_filter, ws!(tag!(")")))
-));
-
-named!(filter_not<Span, Search>, map!(
-        ws!(preceded!(tag!("NOT"), low_filter)),
-        |k|Search::Not(Box::new(k))
-    )
-);
-
-named!(filter_expr<Span, Search>,
-    ws!(exact!(alt_complete!(
-        filter_explicit_or |
-        filter_explicit_and |
-        ws!(filter_implicit_and)
-))));
-
-named!(query_component<Span, Span>, alt_complete!(
-        recognize!(quoted_string) |
-        tag!("AND") |
-        tag!("NOT") |
-        tag!("OR") |
-        tag!("(") |
-        tag!(")") |
-        recognize!(keyword)
-    )
-);
+/// Parses the comparison operators
+fn comp_op(input: Span) -> IResult<Span, ComparisonOp> {
+    alt((
+        tag("==").map(|_| ComparisonOp::Eq),
+        tag("!=").map(|_| ComparisonOp::Neq),
+        tag(">=").map(|_| ComparisonOp::Gte),
+        tag("<=").map(|_| ComparisonOp::Lte),
+        tag(">").map(|_| ComparisonOp::Gt),
+        tag("<").map(|_| ComparisonOp::Lt),
+    ))(input)
+}
 
-named!(end_of_query<Span, Span>, alt!(
-    ws!(tag!("|")) | ws!(eof!())
-));
+/// Parses the unary operators
+fn unary_op(input: Span) -> IResult<Span, UnaryOp> {
+    tag("!").map(|_| UnaryOp::Not).parse(input)
+}
 
-named!(query_section<Span, Span>, recognize!(ws!(
-    many_till!(ws!(query_component), peek!(ws!(end_of_query))))
-));
+fn muldiv_op(input: Span) -> IResult<Span, ArithmeticOp> {
+    alt((
+        tag("*").map(|_| ArithmeticOp::Multiply),
+        tag("/").map(|_| ArithmeticOp::Divide),
+    ))(input)
+}
 
-named!(erroring_filter<Span, Search>, add_return_error!(SyntaxErrors::InvalidFilter.into(), filter_expr));
+/// Parses a chain of multiple/divide expressions
+fn term(input: Span) -> IResult<Span, Expr> {
+    let (input, init) = unary(input)?;
+    let qc = &input.extra;
+
+    let retval = fold_many0(
+        pair(with_pos(muldiv_op).delimited_by(multispace0), opt(unary)).map(
+            |(pos_op, opt_right)| {
+                if let Some(right) = opt_right {
+                    (pos_op.value, right)
+                } else {
+                    qc.report_error_for("expecting an operand for binary operator")
+                        .with_code_range(pos_op.range, "dangling binary operator")
+                        .with_resolution("Add the operand or delete the operator")
+                        .send_report();
+                    (pos_op.value, Expr::Error)
+                }
+            },
+        ),
+        init,
+        |left, (op, right)| Expr::Binary {
+            left: Box::new(left),
+            op: BinaryOp::Arithmetic(op),
+            right: Box::new(right),
+        },
+    )(input);
+
+    retval
+}
 
-named!(prelexed_query<Span, Search>, ws!(do_parse!(
-    res_tuple: flat_map!(query_section, erroring_filter) >>
-    (res_tuple)
-)));
+/// Parses a chain of plus/minus expressions
+fn arith_expr(input: Span) -> IResult<Span, Expr> {
+    let (input, init) = term(input)?;
+
+    fold_many0(
+        pair(
+            addsub_op.delimited_by(multispace0),
+            expect_fn(term, |qc, r| {
+                qc.report_error_for("expecting an operand for binary operator")
+                    .with_code_range(r, "- dangling binary operator")
+                    .with_resolution("Add the operand or delete the operator")
+                    .send_report()
+            })
+            .map(|e| e.unwrap_or(Expr::Error)),
+        ),
+        init,
+        |left, (op, right)| Expr::Binary {
+            left: Box::new(left),
+            op: BinaryOp::Arithmetic(op),
+            right: Box::new(right),
+        },
+    )(input)
+}
+
+/// Parses an expression, optionally, including a comparison operation
+fn opt_expr(input: Span) -> IResult<Span, Expr> {
+    let cmp = map(
+        arith_expr.and(opt(pair(
+            delimited(multispace0, comp_op, multispace0),
+            expect(
+                arith_expr,
+                "expecting the right-hand-side of the comparison",
+            ),
+        ))),
+        |(left, opt_right)| match opt_right {
+            None => left,
+            Some((op, right)) => Expr::Binary {
+                left: Box::new(left),
+                op: BinaryOp::Comparison(op),
+                right: Box::new(right.unwrap_or(Expr::Error)),
+            },
+        },
+    );
+
+    cmp.preceded_by(multispace0).parse(input)
+}
 
-named!(pub query<Span, Query, SyntaxErrors>, fix_error!(SyntaxErrors, exact!(ws!(do_parse!(
-    filter: prelexed_query >>
-    operators: ws!(opt!(preceded!(tag!("|"), operator_list))) >>
-    (Query{
-        search: filter,
-        operators: operators.unwrap_or_default()
+/// Parses a required expression
+fn expr(input: Span) -> IResult<Span, Expr> {
+    Ok(opt_expr(input).unwrap_or_else(|_| {
+        let mut r = input.to_sync_point();
+
+        if r.is_empty() && r.start > 0 {
+            r.start -= 1;
+        }
+        input
+            .extra
+            .report_error_for("expecting an expression")
+            .with_code_range(r.clone(), "")
+            .send_report();
+
+        let end = r.end - input.location_offset();
+        (input.slice(end..), Expr::Error)
     }))
-))));
+}
+
+fn req_ident(input: Span) -> IResult<Span, String> {
+    expect_fn(ident, |qc, r| {
+        qc.report_error_for("expecting a name for the field")
+            .with_code_range(r, "")
+            .with_resolution("Give the value a name")
+            .send_report();
+    })
+    .map(|opt_id| opt_id.unwrap_or_else(|| "".to_string()))
+    .parse(input)
+}
 
-named!(pub operator_list<Span, Vec<Operator>>, ws!(separated_nonempty_list!(tag!("|"), operator)));
+fn quoted_string(input: Span) -> IResult<Span, String> {
+    let sq_esc = escaped(none_of("\\\'"), '\\', tag("'"));
+    let sq_esc_or_empty = alt((sq_esc, tag("")));
+    let single_quoted = expect_delimited(tag("'"), sq_esc_or_empty, tag("'"), |qc, r| {
+        qc.report_error_for("unterminated single quoted string")
+            .with_code_range(r, "")
+            .with_resolution("Insert a single quote (') to terminate this string")
+            .send_report()
+    });
+
+    let dq_esc = escaped(none_of("\\\""), '\\', tag("\""));
+    let dq_esc_or_empty = alt((dq_esc, tag("")));
+    let double_quoted = expect_delimited(tag("\""), dq_esc_or_empty, tag("\""), |qc, r| {
+        qc.report_error_for("unterminated double quoted string")
+            .with_code_range(r, "")
+            .with_resolution("Insert a double quote (\") to terminate this string")
+            .send_report()
+    });
+
+    alt((single_quoted, double_quoted))
+        .map(|s: Span| s.to_string())
+        .parse(input)
+}
 
-#[cfg(test)]
-mod tests {
-    use super::*;
+fn req_quoted_string(input: Span) -> IResult<Span, String> {
+    let upto_ws = input.to_whitespace();
+
+    match quoted_string(input) {
+        Ok(found) => Ok(found),
+        Err(_) => {
+            input
+                .extra
+                .report_error_for("expecting a quoted string")
+                .with_code_range(upto_ws.clone(), "")
+                .with_resolution("Enclose the text in a single or double-quoted string")
+                .send_report();
+
+            Ok((input.slice(upto_ws.len()..), "".to_string()))
+        }
+    }
+}
+
+fn var_list(input: Span) -> IResult<Span, Vec<String>> {
+    separated_list1(tag(","), ident.preceded_by(multispace0))(input)
+}
+
+fn parse(input: Span) -> IResult<Span, Positioned<InlineOperator>> {
+    with_pos(
+        tuple((
+            tag("parse").precedes(multispace1.precedes(req_quoted_string)),
+            opt(multispace1.precedes(with_pos(pair(tag("from"), multispace1).precedes(expr)))),
+            tag("as").preceded_by(multispace1).precedes(var_list),
+            opt(multispace1.precedes(with_pos(pair(tag("from"), multispace1).precedes(expr)))),
+            opt(tag("nodrop").preceded_by(multispace1)).map(|nd| nd.is_some()),
+        ))
+        .map(|(s, e1, fields, e2, no_drop)| InlineOperator::Parse {
+            pattern: Keyword::new_wildcard(s),
+            fields,
+            input_column: (e1, e2),
+            no_drop,
+        }),
+    )
+    .terminated(expect_pipe(
+        "unrecognized option, only the 'from' and 'nodrop' options are available",
+    ))
+    .parse(input)
+}
+
+fn fields_mode(input: Span) -> IResult<Span, FieldMode> {
+    alt((
+        alt((tag("+"), tag("only"), tag("include"))).map(|_| FieldMode::Only),
+        alt((tag("-"), tag("except"), tag("drop"))).map(|_| FieldMode::Except),
+    ))(input)
+}
+
+fn fields(input: Span) -> IResult<Span, Positioned<InlineOperator>> {
+    with_pos(
+        tuple((
+            tag("fields")
+                .precedes(multispace1)
+                .precedes(opt(fields_mode).map(|m| m.unwrap_or(FieldMode::Only))),
+            var_list,
+        ))
+        .map(|(mode, fields)| InlineOperator::Fields { mode, fields }),
+    )
+    .parse(input)
+}
 
-    macro_rules! expect {
-        ($f:expr, $inp:expr, $res:expr) => {{
-            let parse_result = $f(Span::new(CompleteStr($inp)));
-            match parse_result {
+pub fn with_pos<'a, O, E: ParseError<Span<'a>>, F>(
+    mut f: F,
+) -> impl FnMut(Span<'a>) -> IResult<Span<'a>, Positioned<O>, E>
+where
+    F: Parser<Span<'a>, O, E>,
+{
+    move |input: Span<'a>| {
+        let (input, start) = position(input)?;
+        match f.parse(input) {
+            Ok((i, o)) => {
+                let (i, end) = position(i)?;
                 Ok((
-                    LocatedSpan {
-                        fragment: leftover, ..
+                    i,
+                    Positioned {
+                        range: Range {
+                            start: start.location_offset(),
+                            end: end.location_offset(),
+                        },
+                        value: o,
                     },
-                    actual_result,
-                )) => {
-                    assert_eq!(actual_result, $res);
-                    assert_eq!(leftover, CompleteStr(""));
-                }
-                Err(e) => panic!(
-                    "{}",
-                    format!("Parse failed, but was expected to succeed: \n{:?}", e)
-                ),
+                ))
             }
-        }};
+            Err(e) => Err(e),
+        }
     }
+}
 
-    macro_rules! expect_fail {
-        ($f:expr, $inp:expr) => {{
-            let parse_result = $f(Span::new(CompleteStr($inp)));
-            match parse_result {
-                Ok(_res) => panic!("{}", format!("Expected parse to fail, but it succeeded")),
-                // TODO: enable assertions of specific errors
-                Err(_e) => (),
-            }
-        }};
+fn oper_0_args(name: &'static str) -> impl Clone + Fn(Span) -> IResult<Span, Span> {
+    move |input: Span| {
+        tag(name)
+            .and(expect_fn(not(tag("(")), |qc, r| {
+                qc.report_error_for(format!(
+                    "{} does not take any parenthesized arguments",
+                    name
+                ))
+                .with_code_range(r, "")
+                .send_report()
+            }))
+            .precedes(alt((peek(multispace1), end_of_query)))
+            .parse(input)
     }
+}
 
-    #[test]
-    fn empty_query_is_query() {
-        expect!(prelexed_query, " * ", Search::And(vec![]));
-        expect!(prelexed_query, "*", Search::And(vec![]));
-        expect!(
-            prelexed_query,
-            "abc ",
-            Search::And(vec![Search::Keyword(Keyword::new_wildcard(
-                "abc".to_string()
-            ))])
-        );
-        expect!(
-            prelexed_query,
-            "(abc )",
-            Search::And(vec![Search::Keyword(Keyword::new_wildcard(
-                "abc".to_string()
-            ))])
-        );
+/// Parser that consumes characters up to the end of the query
+fn skip_to_end_of_query(input: Span) -> IResult<Span, Operator> {
+    if input.extra.get_error_count() > 0 {
+        many_till(anychar, end_of_query)
+            .map(|_| Operator::Error)
+            .parse(input)
+    } else {
+        satisfy(|_| false).map(|_| Operator::Error).parse(input)
     }
+}
 
-    #[test]
-    fn parse_keyword_string() {
-        expect!(keyword, "abc", "abc".to_string());
-        expect!(keyword, "one-two-three", "one-two-three".to_string());
-    }
+fn parse_operators(input: Span) -> IResult<Span, Vec<Operator>> {
+    let json = with_pos(
+        oper_0_args("json")
+            .precedes(kw_expr("from", "a JSON-encoded string"))
+            .terminated(expect_pipe(
+                "unrecognized option, only the 'from' option is available",
+            ))
+            .map(|input_column| InlineOperator::Json { input_column }),
+    );
+    let limit = with_pos(
+        oper_0_args("limit")
+            .precedes(opt(with_pos(double).preceded_by(multispace1)))
+            .terminated(expect_pipe(
+                "unrecognized option, only the numeric limit can be specified",
+            ))
+            .map(|count| InlineOperator::Limit { count }),
+    );
+    let logfmt = with_pos(
+        oper_0_args("logfmt")
+            .precedes(kw_expr("from", "a logfmt-serialized string"))
+            .terminated(expect_pipe(
+                "unrecognized option, only the 'from' option is available",
+            ))
+            .map(|input_column| InlineOperator::Logfmt { input_column }),
+    );
+    let split = with_pos(
+        tag("split")
+            .precedes(tuple((
+                opt(single_arg("the string to split")),
+                opt(tag("on")
+                    .delimited_by(multispace1)
+                    .precedes(req_quoted_string)),
+                opt(tag("as").delimited_by(multispace1).precedes(expr)),
+            )))
+            .terminated(expect_pipe(
+                "unrecognized option, only the 'on' and 'as' options are available",
+            ))
+            .map(|(e, o, a)| InlineOperator::Split {
+                separator: o.unwrap_or_else(|| ",".to_string()),
+                input_column: e.clone(),
+                output_column: a.or(e),
+            }),
+    );
+    let timeslice = with_pos(
+        tuple((
+            tag("timeslice").precedes(req_single_arg("the date-time value for the log message")),
+            opt(duration.preceded_by(multispace1)),
+            opt(tag("as").delimited_by(multispace1).precedes(ident)),
+        ))
+        .terminated(expect_pipe(
+            "unrecognized option, only the 'as' option is available",
+        ))
+        .map(
+            |(input_column, duration, output_column)| InlineOperator::Timeslice {
+                input_column,
+                duration,
+                output_column,
+            },
+        ),
+    );
+    let total = with_pos(
+        tag("total")
+            .precedes(req_single_arg("the value to sum"))
+            .and(
+                opt(tag("as").delimited_by(multispace1).precedes(req_ident))
+                    .map(|i| i.unwrap_or_else(|| "_total".to_string())),
+            )
+            .terminated(expect_pipe(
+                "unrecognized option, only the 'as' option is available",
+            ))
+            .map(|(input_column, output_column)| InlineOperator::Total {
+                input_column,
+                output_column,
+            }),
+    );
+    let wher = with_pos(
+        tag("where")
+            .precedes(opt(delimited(multispace1, with_pos(expr), multispace0)))
+            .terminated(expect_pipe(
+                "unrecognized option, only the condition can be specified",
+            ))
+            .map(|expr| InlineOperator::Where { expr }),
+    );
+
+    let count = with_pos(
+        tag("count")
+            .precedes(opt(single_arg("the value to count")))
+            .map(|condition| AggregateFunction::Count { condition }),
+    );
+    let count_distinct = with_pos(
+        tag("count_distinct")
+            .precedes(opt(with_pos(arg_list)))
+            .map(|column| AggregateFunction::CountDistinct { column }),
+    );
+    let min = with_pos(
+        tag("min")
+            .precedes(req_single_arg("the numeric value to find the minimum of"))
+            .map(|column| AggregateFunction::Min { column }),
+    );
+    let max = with_pos(
+        tag("max")
+            .precedes(req_single_arg("the numeric value to find the maximum of"))
+            .map(|column| AggregateFunction::Max { column }),
+    );
+    let sum = with_pos(
+        tag("sum")
+            .precedes(req_single_arg("the numeric value to find the sum of"))
+            .map(|column| AggregateFunction::Sum { column }),
+    );
+    let pct = with_pos(
+        alt((tag("pct"), tag("percentile"), tag("p")))
+            .precedes(digit1)
+            .and(req_single_arg("the value to compute the percentile"))
+            .map(|(pct, column)| AggregateFunction::Percentile {
+                column,
+                percentile: format!(".{}", pct).parse::<f64>().unwrap(),
+                percentile_str: pct.to_string(),
+            }),
+    );
+    let avg = with_pos(
+        tag("avg")
+            .or(tag("average"))
+            .precedes(req_single_arg("the numeric value to find the average of"))
+            .map(|column| AggregateFunction::Average { column }),
+    );
+
+    let agg_opers = alt((count_distinct, count, min, max, pct, sum, avg))
+        .and(opt(tag("as").delimited_by(multispace1).precedes(req_ident)))
+        .map(|(f, n)| (n.unwrap_or_else(|| f.value.default_name()), f));
+
+    let multi_agg_opers = tuple((
+        separated_list1(tag(","), agg_opers.delimited_by(multispace0)),
+        opt(tag("by")
+            .terminated(multispace1)
+            .precedes(sourced_expr_list))
+        .terminated(end_of_query)
+        .map(|opt_cols| opt_cols.unwrap_or_else(Vec::new)),
+    ))
+    .map(|(aggregate_functions, cols)| {
+        let (key_col_headers, key_cols) = cols.iter().cloned().unzip();
+        Operator::MultiAggregate(MultiAggregateOperator {
+            key_cols,
+            key_col_headers,
+            aggregate_functions,
+        })
+    });
 
-    #[test]
-    fn parse_quoted_string() {
-        expect!(quoted_string, "\"hello\"", "hello");
-        expect!(quoted_string, "'hello'", "hello");
-        expect!(quoted_string, r#""test = [*=*] * ""#, "test = [*=*] * ");
-        expect_fail!(quoted_string, "\"hello'");
-    }
+    let inline_opers = alt((
+        parse, json, logfmt, fields, limit, split, timeslice, total, wher,
+    ))
+    .map(Operator::Inline);
 
-    #[test]
-    fn parse_expr() {
-        expect!(
-            expr,
-            "a == b",
-            Expr::Binary {
-                op: BinaryOp::Comparison(ComparisonOp::Eq),
-                left: Box::new(Expr::column("a")),
-                right: Box::new(Expr::column("b")),
-            }
-        );
-    }
+    let field_expr = with_pos(
+        expr.and(tag("as").delimited_by(multispace1).precedes(req_ident))
+            .map(|(value, name)| InlineOperator::FieldExpression { value, name }),
+    )
+    .map(Operator::Inline);
 
-    #[test]
-    fn parse_func_call() {
-        expect!(
-            field_expr,
-            "parseDate(abc) as foo",
-            Positioned {
-                start_pos: QueryPosition(0),
-                end_pos: QueryPosition(21),
-                value: InlineOperator::FieldExpression {
-                    value: Expr::FunctionCall {
-                        name: "parseDate".to_string(),
-                        args: vec!(Expr::Column {
-                            head: DataAccessAtom::Key("abc".to_string()),
-                            rest: vec!()
-                        })
+    let alias = recognize(ident).map_res(|span| {
+        AliasPipeline::matching_string(span.fragment())
+            .map(|pipe| Operator::RenderedAlias(pipe.render()))
+    });
+
+    let unknown_op = recognize(ident).terminated(opt(arg_list));
+
+    let did_you_mean = separated_list1(tag(","), unknown_op.delimited_by(multispace0))
+        .terminated(
+            opt(tag("by")
+                .terminated(multispace1)
+                .precedes(sourced_expr_list))
+            .terminated(many_till(anychar, end_of_query)),
+        )
+        .map(|unknown_ids| {
+            let is_agg = unknown_ids.len() > 1;
+            for i in unknown_ids {
+                if is_agg {
+                    if VALID_AGGREGATES.contains(&i) {
+                        continue;
+                    }
+                } else if VALID_OPERATORS.contains(&i) {
+                    continue;
+                }
+
+                let m = crate::errors::did_you_mean(
+                    &i,
+                    if is_agg {
+                        &VALID_AGGREGATES
+                    } else {
+                        &VALID_OPERATORS
                     },
-                    name: "foo".to_string()
+                );
+
+                let mut builder = input
+                    .extra
+                    .report_error_for(if is_agg {
+                        "Not an aggregate operator"
+                    } else {
+                        "Expected an operator"
+                    })
+                    .with_code_range(i.to_range(), "")
+                    .with_resolution(if is_agg {
+                        format!("{} is not a valid aggregate operator", i)
+                    } else {
+                        format!("{} is not a valid operator", i)
+                    });
+
+                if m.is_some() {
+                    builder = builder.with_resolution(format!("Did you mean \"{}\"?", m.unwrap()));
+                } else if is_agg && VALID_INLINE.contains(&i) {
+                    builder = builder.with_resolution(format!("{} is an inline operator, but only aggregate operators (count, average, etc.) are valid here", i));
                 }
+                builder.send_report();
             }
-        );
+
+            Operator::Error
+        });
+
+    let garbage = expect(alt((tag("|"), eof)), "unrecognized syntax").map(|_| Operator::Error);
+
+    let opers = alt((
+        inline_opers,
+        multi_agg_opers,
+        sort,
+        field_expr,
+        alias,
+        skip_to_end_of_query,
+        did_you_mean,
+        garbage,
+    ));
+
+    separated_list1(tag("|"), opers.delimited_by(multispace0)).parse(input)
+}
+
+pub fn pipeline_template(input: &QueryContainer) -> Result<Vec<Operator>, ()> {
+    let span = Span::new_extra(input.query.as_str(), input);
+    let (_input, operators) = parse_operators(span).map_err(|_| ())?;
+
+    Ok(operators)
+}
+
+pub fn query(input: &QueryContainer) -> Result<Query, ()> {
+    let span = Span::new_extra(input.query.as_str(), input);
+    let (input, search) = parse_search(span).map_err(|_| ())?;
+    let (input, operators) = opt(tag("|").precedes(parse_operators))
+        .map(|ops| ops.unwrap_or_default())
+        .parse(input)
+        .map_err(|_| ())?;
+
+    if input.extra.get_error_count() > 0 {
+        Err(())
+    } else {
+        Ok(Query { search, operators })
     }
+}
 
-    #[test]
-    fn parse_expr_value() {
-        expect!(
-            expr,
-            "a <= \"b\"",
-            Expr::Binary {
-                op: BinaryOp::Comparison(ComparisonOp::Lte),
-                left: Box::new(Expr::column("a")),
-                right: Box::new(Expr::Value(data::Value::Str("b".to_string()))),
-            }
-        );
+#[cfg(test)]
+mod tests {
+    use crate::errors::ErrorReporter;
+    use annotate_snippets::snippet::Snippet;
+    use expect_test::{expect, Expect};
+
+    use super::*;
+    use std::sync::{Arc, Mutex};
+
+    fn check(actual: (Query, &Vec<String>), expect: Expect) {
+        let actual_errors = actual.1.join("\n");
+        let actual_combined = format!("{:#?}\n{}", actual.0, actual_errors);
+        expect.assert_eq(&actual_combined);
     }
 
-    #[test]
-    fn parse_expr_ident() {
-        expect!(expr, "foo", Expr::column("foo"));
-        expect!(
-            expr,
-            "foo.bar.baz[-10]",
-            Expr::Column {
-                head: DataAccessAtom::Key("foo".to_owned()),
-                rest: vec![
-                    DataAccessAtom::Key("bar".to_owned()),
-                    DataAccessAtom::Key("baz".to_owned()),
-                    DataAccessAtom::Index(-10)
-                ]
+    fn check_query(query_in: &str, expect: Expect) {
+        let errors = Arc::new(Mutex::new(vec![]));
+        let parsed = {
+            let qc = QueryContainer::new(
+                query_in.to_string(),
+                Box::new(VecErrorReporter::new(errors.clone())),
+            );
+            let r = query(&qc);
+
+            if r.is_ok() {
+                r.unwrap()
+            } else {
+                Query {
+                    search: Search::And(vec![]),
+                    operators: vec![],
+                }
             }
-        )
+        };
+
+        {
+            let errvec = errors.lock().unwrap();
+
+            check((parsed, errvec.as_ref()), expect);
+        }
     }
 
-    #[test]
-    fn parse_expr_precedence() {
-        expect!(
-            expr,
-            "2 * 3 + 4 * 5 < 4 / 2 + 1",
-            Expr::Binary {
-                left: Box::new(Expr::Binary {
-                    left: Box::new(Expr::Binary {
-                        left: Box::new(Expr::Value(data::Value::Int(2))),
-                        op: BinaryOp::Arithmetic(ArithmeticOp::Multiply),
-                        right: Box::new(Expr::Value(data::Value::Int(3))),
-                    }),
-                    op: BinaryOp::Arithmetic(ArithmeticOp::Add),
-                    right: Box::new(Expr::Binary {
-                        left: Box::new(Expr::Value(data::Value::Int(4))),
-                        op: BinaryOp::Arithmetic(ArithmeticOp::Multiply),
-                        right: Box::new(Expr::Value(data::Value::Int(5))),
-                    }),
-                }),
-                op: BinaryOp::Comparison(ComparisonOp::Lt),
-                right: Box::new(Expr::Binary {
-                    left: Box::new(Expr::Binary {
-                        left: Box::new(Expr::Value(data::Value::Int(4))),
-                        op: BinaryOp::Arithmetic(ArithmeticOp::Divide),
-                        right: Box::new(Expr::Value(data::Value::Int(2))),
-                    }),
-                    op: BinaryOp::Arithmetic(ArithmeticOp::Add),
-                    right: Box::new(Expr::Value(data::Value::Int(1))),
-                })
-            }
-        );
+    struct VecErrorReporter {
+        errors: Arc<Mutex<Vec<String>>>,
     }
 
-    #[test]
-    fn parse_ident() {
-        expect!(ident, "hello123", "hello123".to_string());
-        expect!(ident, "x", "x".to_string());
-        expect!(ident, "_x", "_x".to_string());
-        expect_fail!(ident, "5x");
+    impl ErrorReporter for VecErrorReporter {
+        fn handle_error(&self, snippet: Snippet) {
+            let dl = annotate_snippets::display_list::DisplayList::from(snippet);
+
+            self.errors.lock().unwrap().push(format!("{}", dl));
+        }
     }
 
-    #[test]
-    fn parse_quoted_ident() {
-        expect!(ident, "[\"hello world\"]", "hello world".to_string());
-        expect!(ident, "[\"hello.world\"]", "hello.world".to_string());
-        expect!(
-            ident,
-            r#"["hello \"world\""]"#,
-            r#"hello \"world\""#.to_string()
-        );
-        expect_fail!(ident, "\"\"");
+    impl VecErrorReporter {
+        fn new(errors: Arc<Mutex<Vec<String>>>) -> Self {
+            VecErrorReporter { errors }
+        }
     }
 
     #[test]
-    fn parse_var_list() {
-        expect!(
-            var_list,
-            "a, b, def, g_55",
-            vec![
-                "a".to_string(),
-                "b".to_string(),
-                "def".to_string(),
-                "g_55".to_string(),
-            ]
+    fn empty_query_is_query() {
+        check_query(
+            "*",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+            "#]],
+        );
+
+        check_query(
+            " * ",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+            "#]],
         );
     }
 
     #[test]
-    fn parse_parses() {
-        expect!(
-            parse,
-            r#"parse "[key=*]" as v"#,
-            Positioned {
-                start_pos: QueryPosition(0),
-                end_pos: QueryPosition(20),
-                value: InlineOperator::Parse {
-                    pattern: Keyword::new_wildcard("[key=*]".to_string()),
-                    fields: vec!["v".to_string()],
-                    input_column: (None, None),
-                    no_drop: false
-                }
+    fn parse_ident() {
+        check_query(
+            "* | 1 as hello123",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..17,
+                            value: FieldExpression {
+                                value: Value(
+                                    Int(
+                                        1,
+                                    ),
+                                ),
+                                name: "hello123",
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
         );
-        expect!(
-            parse,
-            r#"parse "[key=*]" as v nodrop"#,
-            Positioned {
-                start_pos: QueryPosition(0),
-                end_pos: QueryPosition(27),
-                value: InlineOperator::Parse {
-                    pattern: Keyword::new_wildcard("[key=*]".to_string()),
-                    fields: vec!["v".to_string()],
-                    input_column: (None, None),
-                    no_drop: true
-                }
+        check_query(
+            "* | 1 as x",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..10,
+                            value: FieldExpression {
+                                value: Value(
+                                    Int(
+                                        1,
+                                    ),
+                                ),
+                                name: "x",
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
         );
-        expect!(
-            parse,
-            r#"parse "[key=*][val=*]" as k,v nodrop"#,
-            Positioned {
-                start_pos: QueryPosition(0),
-                end_pos: QueryPosition(36),
-                value: InlineOperator::Parse {
-                    pattern: Keyword::new_wildcard("[key=*][val=*]".to_string()),
-                    fields: vec!["k".to_string(), "v".to_string()],
-                    input_column: (None, None),
-                    no_drop: true
-                }
+        check_query(
+            "* | 1 as _x",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..11,
+                            value: FieldExpression {
+                                value: Value(
+                                    Int(
+                                        1,
+                                    ),
+                                ),
+                                name: "_x",
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
+        );
+        check_query(
+            "* | 1 as 5x",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: expecting a name for the field
+                  |
+                1 | * | 1 as 5x
+                  |          ^^
+                  |
+                  = help: Give the value a name"#]],
         );
     }
 
     #[test]
-    fn parse_operator() {
-        expect!(
-            operator,
-            r#" 1 as one"#,
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(9),
-                value: InlineOperator::FieldExpression {
-                    value: Expr::Value(data::Value::Int(1)),
-                    name: "one".to_string(),
-                },
-            })
-        );
-        expect!(
-            operator,
-            "  json",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(2),
-                end_pos: QueryPosition(6),
-                value: InlineOperator::Json { input_column: None }
-            })
+    fn parse_parses() {
+        check_query(
+            r#"* | parse "[key=*]" as v"#,
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..24,
+                                value: Parse {
+                                    pattern: Keyword(
+                                        "[key=*]",
+                                        WILDCARD,
+                                    ),
+                                    fields: [
+                                        "v",
+                                    ],
+                                    input_column: (
+                                        None,
+                                        None,
+                                    ),
+                                    no_drop: false,
+                                },
+                            },
+                        ),
+                    ],
+                }
+            "#]],
         );
-        expect!(
-            operator,
-            "  logfmt",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(2),
-                end_pos: QueryPosition(8),
-                value: InlineOperator::Logfmt { input_column: None }
-            })
+        check_query(
+            r#"* | parse "[key=*]" as v nodrop"#,
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..31,
+                                value: Parse {
+                                    pattern: Keyword(
+                                        "[key=*]",
+                                        WILDCARD,
+                                    ),
+                                    fields: [
+                                        "v",
+                                    ],
+                                    input_column: (
+                                        None,
+                                        None,
+                                    ),
+                                    no_drop: true,
+                                },
+                            },
+                        ),
+                    ],
+                }
+            "#]],
         );
-        expect!(
-            operator,
-            r#" parse "[key=*]" from field as v"#,
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(32),
-                value: InlineOperator::Parse {
-                    pattern: Keyword::new_wildcard("[key=*]".to_string()),
-                    fields: vec!["v".to_string()],
-                    input_column: (
-                        Some(Positioned {
-                            start_pos: QueryPosition(17),
-                            end_pos: QueryPosition(28),
-                            value: Expr::column("field")
-                        }),
-                        None
+        check_query(
+            r#"* | parse "[key=*][val=*]" as k,v nodrop"#,
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..40,
+                            value: Parse {
+                                pattern: Keyword(
+                                    "[key=*][val=*]",
+                                    WILDCARD,
+                                ),
+                                fields: [
+                                    "k",
+                                    "v",
+                                ],
+                                input_column: (
+                                    None,
+                                    None,
+                                ),
+                                no_drop: true,
+                            },
+                        },
                     ),
-                    no_drop: false
-                },
-            })
+                ],
+            }
+        "#]],
         );
-        expect!(
-            operator,
-            r#" parse "[key=*]" as v from field"#,
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(32),
-                value: InlineOperator::Parse {
-                    pattern: Keyword::new_wildcard("[key=*]".to_string()),
-                    fields: vec!["v".to_string()],
-                    input_column: (
-                        None,
-                        Some(Positioned {
-                            start_pos: QueryPosition(22),
-                            end_pos: QueryPosition(32),
-                            value: Expr::column("field")
-                        })
+        check_query(
+            r#"* | parse "[key=*]" from field as v"#,
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
                     ),
-                    no_drop: false
-                },
-            })
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..35,
+                                value: Parse {
+                                    pattern: Keyword(
+                                        "[key=*]",
+                                        WILDCARD,
+                                    ),
+                                    fields: [
+                                        "v",
+                                    ],
+                                    input_column: (
+                                        Some(
+                                            Positioned {
+                                                range: 20..30,
+                                                value: Column {
+                                                    head: Key(
+                                                        "field",
+                                                    ),
+                                                    rest: [],
+                                                },
+                                            },
+                                        ),
+                                        None,
+                                    ),
+                                    no_drop: false,
+                                },
+                            },
+                        ),
+                    ],
+                }
+            "#]],
         );
-        expect!(
-            operator,
-            r#" timeslice(date) 5m "#,
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(20),
-                value: InlineOperator::Timeslice {
-                    input_column: Expr::column("date"),
-                    duration: Some(chrono::Duration::minutes(5)),
-                    output_column: None,
-                },
-            })
+        check_query(
+            r#"* | parse "[key=*]" as v from field"#,
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..35,
+                                value: Parse {
+                                    pattern: Keyword(
+                                        "[key=*]",
+                                        WILDCARD,
+                                    ),
+                                    fields: [
+                                        "v",
+                                    ],
+                                    input_column: (
+                                        None,
+                                        Some(
+                                            Positioned {
+                                                range: 25..35,
+                                                value: Column {
+                                                    head: Key(
+                                                        "field",
+                                                    ),
+                                                    rest: [],
+                                                },
+                                            },
+                                        ),
+                                    ),
+                                    no_drop: false,
+                                },
+                            },
+                        ),
+                    ],
+                }
+            "#]],
         );
-        expect!(
-            operator,
-            r#" timeslice(date) 5m as theslice "#,
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(32),
-                value: InlineOperator::Timeslice {
-                    input_column: Expr::column("date"),
-                    duration: Some(chrono::Duration::minutes(5)),
-                    output_column: Some("theslice".to_string()),
-                },
-            })
+        check_query(
+            "* | parse 'x=* y=*' as x, y extra",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: unrecognized option, only the 'from' and 'nodrop' options are available
+                  |
+                1 | * | parse 'x=* y=*' as x, y extra
+                  |                             ^^^^^
+                  |"#]],
         );
     }
 
     #[test]
-    fn parse_limit() {
-        expect!(
-            operator,
-            " limit",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(6),
-                value: InlineOperator::Limit { count: None }
-            })
+    fn parse_keyword_string() {
+        check_query(
+            "abc",
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Keyword(
+                            Keyword(
+                                "abc",
+                                WILDCARD,
+                            ),
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
-        expect!(
-            operator,
-            " limit 5",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(8),
-                value: InlineOperator::Limit {
-                    count: Some(Positioned {
-                        value: 5.0,
-                        start_pos: QueryPosition(7),
-                        end_pos: QueryPosition(8)
-                    })
-                }
-            })
+        check_query(
+            "one-two-three",
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Keyword(
+                            Keyword(
+                                "one-two-three",
+                                WILDCARD,
+                            ),
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
-        expect!(
-            operator,
-            " limit -5",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(9),
-                value: InlineOperator::Limit {
-                    count: Some(Positioned {
-                        value: -5.0,
-                        start_pos: QueryPosition(7),
-                        end_pos: QueryPosition(9)
-                    }),
-                }
-            })
+    }
+
+    #[test]
+    fn parse_expr() {
+        check_query(
+            "* | where foo *  ",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [],
+            }
+            error: expecting an operand for binary operator
+              |
+            1 | * | where foo *  
+              |               ^ dangling binary operator
+              |
+              = help: Add the operand or delete the operator"#]],
         );
-        expect!(
-            operator,
-            " limit 1e2",
-            Operator::Inline(Positioned {
-                start_pos: QueryPosition(1),
-                end_pos: QueryPosition(10),
-                value: InlineOperator::Limit {
-                    count: Some(Positioned {
-                        value: 1e2,
-                        start_pos: QueryPosition(7),
-                        end_pos: QueryPosition(10)
-                    })
+    }
+
+    #[test]
+    fn parse_func_call() {
+        check_query(
+            "* | parseDate(abc) as foo",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..25,
+                                value: FieldExpression {
+                                    value: FunctionCall {
+                                        name: "parseDate",
+                                        args: [
+                                            Column {
+                                                head: Key(
+                                                    "abc",
+                                                ),
+                                                rest: [],
+                                            },
+                                        ],
+                                    },
+                                    name: "foo",
+                                },
+                            },
+                        ),
+                    ],
                 }
-            })
+            "#]],
+        );
+        check_query(
+            "* | parseDate(abc 123) as foo",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [],
+            }
+            error: unhandled input
+              |
+            1 | * | parseDate(abc 123) as foo
+              |                   ^^^
+              |"#]],
         );
     }
 
     #[test]
-    fn parse_agg_operator() {
-        expect!(
-            multi_aggregate_operator,
-            "count as renamed by x, y",
-            Operator::MultiAggregate(MultiAggregateOperator {
-                key_cols: vec![Expr::column("x"), Expr::column("y")],
-                key_col_headers: vec!["x".to_string(), "y".to_string()],
-                aggregate_functions: vec![(
-                    "renamed".to_string(),
-                    Positioned {
-                        value: AggregateFunction::Count { condition: None },
-                        start_pos: QueryPosition(0),
-                        end_pos: QueryPosition(6)
-                    }
-                )],
-            })
+    fn parse_expr_precedence() {
+        check_query(
+            "* | 2 * 3 + 4 * 5 < 4 / 2 + 1 as val",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..36,
+                                value: FieldExpression {
+                                    value: Binary {
+                                        op: Comparison(
+                                            Lt,
+                                        ),
+                                        left: Binary {
+                                            op: Arithmetic(
+                                                Add,
+                                            ),
+                                            left: Binary {
+                                                op: Arithmetic(
+                                                    Multiply,
+                                                ),
+                                                left: Value(
+                                                    Int(
+                                                        2,
+                                                    ),
+                                                ),
+                                                right: Value(
+                                                    Int(
+                                                        3,
+                                                    ),
+                                                ),
+                                            },
+                                            right: Binary {
+                                                op: Arithmetic(
+                                                    Multiply,
+                                                ),
+                                                left: Value(
+                                                    Int(
+                                                        4,
+                                                    ),
+                                                ),
+                                                right: Value(
+                                                    Int(
+                                                        5,
+                                                    ),
+                                                ),
+                                            },
+                                        },
+                                        right: Binary {
+                                            op: Arithmetic(
+                                                Add,
+                                            ),
+                                            left: Binary {
+                                                op: Arithmetic(
+                                                    Divide,
+                                                ),
+                                                left: Value(
+                                                    Int(
+                                                        4,
+                                                    ),
+                                                ),
+                                                right: Value(
+                                                    Int(
+                                                        2,
+                                                    ),
+                                                ),
+                                            },
+                                            right: Value(
+                                                Int(
+                                                    1,
+                                                ),
+                                            ),
+                                        },
+                                    },
+                                    name: "val",
+                                },
+                            },
+                        ),
+                    ],
+                }
+            "#]],
         );
     }
 
     #[test]
-    fn parse_percentile() {
-        expect!(
-            complete_agg_function,
-            "p50(x)",
-            (
-                "p50".to_string(),
-                Positioned {
-                    value: AggregateFunction::Percentile {
-                        column: Expr::column("x"),
-                        percentile: 0.5,
-                        percentile_str: "50".to_string(),
-                    },
-                    start_pos: QueryPosition(0),
-                    end_pos: QueryPosition(6),
-                }
-            )
+    fn complex_filters() {
+        check_query(
+            " abc def \"*ghi*\" ",
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Keyword(
+                            Keyword(
+                                "abc",
+                                WILDCARD,
+                            ),
+                        ),
+                        Keyword(
+                            Keyword(
+                                "def",
+                                WILDCARD,
+                            ),
+                        ),
+                        Keyword(
+                            Keyword(
+                                "*ghi*",
+                                EXACT,
+                            ),
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
+        );
+        check_query(
+            "(abc AND def) OR xyz",
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Or(
+                            [
+                                And(
+                                    [
+                                        Keyword(
+                                            Keyword(
+                                                "abc",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                        Keyword(
+                                            Keyword(
+                                                "def",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                    ],
+                                ),
+                                Keyword(
+                                    Keyword(
+                                        "xyz",
+                                        WILDCARD,
+                                    ),
+                                ),
+                            ],
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
     }
 
     #[test]
-    fn query_no_operators() {
-        expect!(
-            query,
-            " * ",
+    fn parse_limit() {
+        check_query(
+            "* | limit",
+            expect![[r#"
             Query {
-                search: Search::And(vec![]),
-                operators: vec![],
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..9,
+                            value: Limit {
+                                count: None,
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
         );
-        expect!(
-            query,
-            " filter ",
+        check_query(
+            "* | limit 5",
+            expect![[r#"
             Query {
-                search: Search::And(vec![Search::Keyword(Keyword::new_wildcard(
-                    "filter".to_string()
-                ))]),
-                operators: vec![],
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..11,
+                            value: Limit {
+                                count: Some(
+                                    Positioned {
+                                        range: 10..11,
+                                        value: 5.0,
+                                    },
+                                ),
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
         );
-        expect!(
-            query,
-            " *abc* ",
+        check_query(
+            "* | limit -5",
+            expect![[r#"
             Query {
-                search: Search::And(vec![Search::Keyword(Keyword::new_wildcard(
-                    "abc".to_string()
-                ))]),
-                operators: vec![],
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..12,
+                            value: Limit {
+                                count: Some(
+                                    Positioned {
+                                        range: 10..12,
+                                        value: -5.0,
+                                    },
+                                ),
+                            },
+                        },
+                    ),
+                ],
             }
+        "#]],
         );
-        expect!(
-            query,
-            " abc def \"*ghi*\" ",
+        check_query(
+            "* | limit 1e2",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..13,
+                            value: Limit {
+                                count: Some(
+                                    Positioned {
+                                        range: 10..13,
+                                        value: 100.0,
+                                    },
+                                ),
+                            },
+                        },
+                    ),
+                ],
+            }
+        "#]],
+        );
+        check_query(
+            "* | limit foo",
+            expect![[r#"
             Query {
-                search: Search::And(vec![
-                    Search::Keyword(Keyword::new_wildcard("abc".to_string())),
-                    Search::Keyword(Keyword::new_wildcard("def".to_string())),
-                    Search::Keyword(Keyword::new_exact("*ghi*".to_string())),
-                ]),
-                operators: vec![],
+                search: And(
+                    [],
+                ),
+                operators: [],
             }
+            error: unrecognized option, only the numeric limit can be specified
+              |
+            1 | * | limit foo
+              |           ^^^
+              |"#]],
         );
     }
 
     #[test]
-    fn complex_filters() {
-        expect!(
-            filter_expr,
-            "(abc AND def) OR xyz",
-            Search::Or(vec![
-                Search::And(vec![
-                    Search::Keyword(Keyword::new_wildcard("abc".to_string())),
-                    Search::Keyword(Keyword::new_wildcard("def".to_string()))
-                ]),
-                Search::Keyword(Keyword::new_wildcard("xyz".to_string()))
-            ])
+    fn parse_agg_operator() {
+        check_query(
+            "* | count()",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: this operator takes 1 argument, but 0 were supplied
+                  |
+                1 | * | count()
+                  |            - supplied 0 arguments
+                  |
+                  = help: the argument should supply the value to count"#]],
+        );
+        check_query(
+            "* | json | count, count_distinct(message) by level",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [
+                        Inline(
+                            Positioned {
+                                range: 4..8,
+                                value: Json {
+                                    input_column: None,
+                                },
+                            },
+                        ),
+                        MultiAggregate(
+                            MultiAggregateOperator {
+                                key_cols: [
+                                    Column {
+                                        head: Key(
+                                            "level",
+                                        ),
+                                        rest: [],
+                                    },
+                                ],
+                                key_col_headers: [
+                                    "level",
+                                ],
+                                aggregate_functions: [
+                                    (
+                                        "_count",
+                                        Positioned {
+                                            range: 11..16,
+                                            value: Count {
+                                                condition: None,
+                                            },
+                                        },
+                                    ),
+                                    (
+                                        "_countDistinct",
+                                        Positioned {
+                                            range: 18..41,
+                                            value: CountDistinct {
+                                                column: Some(
+                                                    Positioned {
+                                                        range: 32..41,
+                                                        value: [
+                                                            Column {
+                                                                head: Key(
+                                                                    "message",
+                                                                ),
+                                                                rest: [],
+                                                            },
+                                                        ],
+                                                    },
+                                                ),
+                                            },
+                                        },
+                                    ),
+                                ],
+                            },
+                        ),
+                    ],
+                }
+            "#]],
+        );
+        check_query(
+            "* | count as renamed by x, y",
+            expect![[r#"
+            Query {
+                search: And(
+                    [],
+                ),
+                operators: [
+                    MultiAggregate(
+                        MultiAggregateOperator {
+                            key_cols: [
+                                Column {
+                                    head: Key(
+                                        "x",
+                                    ),
+                                    rest: [],
+                                },
+                                Column {
+                                    head: Key(
+                                        "y",
+                                    ),
+                                    rest: [],
+                                },
+                            ],
+                            key_col_headers: [
+                                "x",
+                                "y",
+                            ],
+                            aggregate_functions: [
+                                (
+                                    "renamed",
+                                    Positioned {
+                                        range: 4..9,
+                                        value: Count {
+                                            condition: None,
+                                        },
+                                    },
+                                ),
+                            ],
+                        },
+                    ),
+                ],
+            }
+        "#]],
         );
-    }
-
-    #[test]
-    fn invalid_filters() {
-        expect_fail!(query, "abc AND \"");
-        // Would be nice to support this
-        expect_fail!(query, "a OR b OR C");
     }
 
     #[test]
     fn or_filter() {
-        expect!(
-            filter_expr,
+        check_query(
             "abc OR (def OR xyz)",
-            Search::Or(vec![
-                Search::Keyword(Keyword::new_wildcard("abc".to_string())),
-                Search::Or(vec![
-                    Search::Keyword(Keyword::new_wildcard("def".to_string())),
-                    Search::Keyword(Keyword::new_wildcard("xyz".to_string()))
-                ])
-            ])
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Or(
+                            [
+                                Keyword(
+                                    Keyword(
+                                        "abc",
+                                        WILDCARD,
+                                    ),
+                                ),
+                                Or(
+                                    [
+                                        Keyword(
+                                            Keyword(
+                                                "def",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                        Keyword(
+                                            Keyword(
+                                                "xyz",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                    ],
+                                ),
+                            ],
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
     }
 
     #[test]
     fn not_filter() {
-        expect!(
-            filter_not,
-            "NOT abc",
-            Search::Not(Box::new(Search::Keyword(Keyword::new_wildcard(
-                "abc".to_string()
-            ))))
-        );
-        expect!(
-            filter_expr,
+        check_query(
             "NOT abc",
-            Search::And(vec![Search::Not(Box::new(Search::Keyword(
-                Keyword::new_wildcard("abc".to_string())
-            )))])
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        Not(
+                            Keyword(
+                                Keyword(
+                                    "abc",
+                                    WILDCARD,
+                                ),
+                            ),
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
-        expect!(
-            filter_expr,
+        check_query(
             "(error OR warn) AND NOT hide",
-            Search::And(vec![
-                Search::Or(vec![
-                    Search::Keyword(Keyword::new_wildcard("error".to_string())),
-                    Search::Keyword(Keyword::new_wildcard("warn".to_string()))
-                ]),
-                Search::Not(Box::new(Search::Keyword(Keyword::new_wildcard(
-                    "hide".to_string()
-                )))),
-            ])
+            expect![[r#"
+            Query {
+                search: And(
+                    [
+                        And(
+                            [
+                                Or(
+                                    [
+                                        Keyword(
+                                            Keyword(
+                                                "error",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                        Keyword(
+                                            Keyword(
+                                                "warn",
+                                                WILDCARD,
+                                            ),
+                                        ),
+                                    ],
+                                ),
+                                Not(
+                                    Keyword(
+                                        Keyword(
+                                            "hide",
+                                            WILDCARD,
+                                        ),
+                                    ),
+                                ),
+                            ],
+                        ),
+                    ],
+                ),
+                operators: [],
+            }
+        "#]],
         );
     }
 
     #[test]
-    fn query_operators() {
-        let query_str = r#"* | json from col | parse "!123*" as foo | count by foo, foo == 123 | sort by foo dsc "#;
-        expect!(
-            query,
-            query_str,
-            Query {
-                search: Search::And(vec![]),
-                operators: vec![
-                    Operator::Inline(Positioned {
-                        start_pos: QueryPosition(4),
-                        end_pos: QueryPosition(18),
-                        value: InlineOperator::Json {
-                            input_column: Some(Expr::column("col")),
-                        }
-                    }),
-                    Operator::Inline(Positioned {
-                        start_pos: QueryPosition(20),
-                        end_pos: QueryPosition(41),
-                        value: InlineOperator::Parse {
-                            pattern: Keyword::new_wildcard("!123*".to_string()),
-                            fields: vec!["foo".to_string()],
-                            input_column: (None, None),
-                            no_drop: false
-                        }
-                    }),
-                    Operator::MultiAggregate(MultiAggregateOperator {
-                        key_col_headers: vec!["foo".to_string(), "foo == 123".to_string()],
-                        key_cols: vec![
-                            Expr::column("foo"),
-                            Expr::Binary {
-                                op: BinaryOp::Comparison(ComparisonOp::Eq),
-                                left: Box::new(Expr::column("foo")),
-                                right: Box::new(Expr::Value(data::Value::Int(123))),
-                            },
-                        ],
-                        aggregate_functions: vec![(
-                            "_count".to_string(),
-                            Positioned {
-                                value: AggregateFunction::Count { condition: None },
-                                start_pos: QueryPosition(43),
-                                end_pos: QueryPosition(49),
-                            }
-                        ),],
-                    }),
-                    Operator::Sort(SortOperator {
-                        sort_cols: vec![Expr::column("foo")],
-                        direction: SortMode::Descending,
-                    }),
-                ],
-            }
+    fn unterminated() {
+        check_query(
+            "* | (abc",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: unterminated parenthesized expression
+                  |
+                1 | * | (abc
+                  |     ^^^^ unterminated parenthesized expression
+                  |
+                  = help: Insert a right parenthesis to terminate this expression"#]],
+        );
+
+        check_query(
+            "* | parseDate(abc) | bar",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: Expected an operator
+                  |
+                1 | * | parseDate(abc) | bar
+                  |     ^^^^^^^^^
+                  |
+                  = help: parseDate is not a valid operator"#]],
+        );
+        check_query(
+            "* | where 'abc",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: unterminated single quoted string
+                  |
+                1 | * | where 'abc
+                  |           ^^^^
+                  |
+                  = help: Insert a single quote (') to terminate this string"#]],
+        );
+    }
+
+    #[test]
+    fn split_operator() {
+        check_query(
+            "* | split(abc) on , as foo",
+            expect![[r#"
+                Query {
+                    search: And(
+                        [],
+                    ),
+                    operators: [],
+                }
+                error: expecting a quoted string
+                  |
+                1 | * | split(abc) on , as foo
+                  |                   ^
+                  |
+                  = help: Enclose the text in a single or double-quoted string"#]],
         );
     }
 
     #[test]
     fn logfmt_operator() {
-        let query_str = r#"* | logfmt from col | sort by foo dsc "#;
-        expect!(
-            query,
-            query_str,
+        check_query(
+            r#"* | logfmt from col | sort by foo dsc "#,
+            expect![[r#"
             Query {
-                search: Search::And(vec![]),
-                operators: vec![
-                    Operator::Inline(Positioned {
-                        start_pos: QueryPosition(4),
-                        end_pos: QueryPosition(20),
-                        value: InlineOperator::Logfmt {
-                            input_column: Some(Expr::column("col")),
-                        }
-                    }),
-                    Operator::Sort(SortOperator {
-                        sort_cols: vec![Expr::column("foo")],
-                        direction: SortMode::Descending,
-                    }),
+                search: And(
+                    [],
+                ),
+                operators: [
+                    Inline(
+                        Positioned {
+                            range: 4..19,
+                            value: Logfmt {
+                                input_column: Some(
+                                    Column {
+                                        head: Key(
+                                            "col",
+                                        ),
+                                        rest: [],
+                                    },
+                                ),
+                            },
+                        },
+                    ),
+                    Sort(
+                        SortOperator {
+                            sort_cols: [
+                                Column {
+                                    head: Key(
+                                        "foo",
+                                    ),
+                                    rest: [],
+                                },
+                            ],
+                            direction: Descending,
+                        },
+                    ),
                 ],
             }
+        "#]],
         );
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
index b8a84b3..5556d93 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -14,7 +14,7 @@ mod typecheck;
 
 pub mod pipeline {
     use crate::data::{DisplayConfig, Record, Row};
-    pub use crate::errors::{ErrorReporter, QueryContainer};
+    pub use crate::errors::{ErrorReporter, QueryContainer, TermErrorReporter};
     use crate::filter;
     use crate::lang::*;
     use crate::operator;
@@ -23,8 +23,7 @@ pub mod pipeline {
     use crate::typecheck::{TypeCheck, TypeError};
     use crossbeam_channel::{bounded, Receiver, RecvTimeoutError, Sender};
     use failure::Error;
-    use failure::{bail, Fail};
-    use nom::types::CompleteStr;
+    use failure::Fail;
     use std::collections::VecDeque;
     use std::io::{BufRead, Write};
     use std::thread;
@@ -131,23 +130,12 @@ pub mod pipeline {
             let mut has_errors = false;
             while let Some(op) = op_deque.pop_front() {
                 match op {
+                    Operator::Error => {}
                     Operator::RenderedAlias(rendered_alias) => {
-                        // TODO: create a new QueryContainer here and parse the templated string
-                        // -> expecting only Inline operators?
-                        // let inner_query = QueryContainer::new(rendered_alias.value, ???);
-                        let (_span, operators) =
-                            match operator_list(Span::new(CompleteStr(&rendered_alias.value))) {
-                                Err(_err) => bail!(
-                                    "Failed to parse rendered alias: `{}` as operator_list",
-                                    rendered_alias.value,
-                                ),
-                                Ok(v) => v,
-                            };
-
-                        // Insert operators (in-order) to front of operator stack
-                        for new_op in operators.into_iter().rev() {
-                            op_deque.push_front(new_op);
-                        }
+                        rendered_alias
+                            .into_iter()
+                            .rev()
+                            .for_each(|op| op_deque.push_front(op));
                     }
                     Operator::Inline(inline_op) => {
                         let op_builder = inline_op.type_check(pipeline)?;
diff --git a/src/typecheck.rs b/src/typecheck.rs
index 9f86306..34b5445 100644
--- a/src/typecheck.rs
+++ b/src/typecheck.rs
@@ -129,6 +129,7 @@ impl TypeCheck<operator::Expr> for lang::Expr {
                 let static_value: &'static mut Value = Box::leak(boxed);
                 Ok(operator::Expr::Value(static_value))
             }
+            lang::Expr::Error => Err(TypeError::ExpectedExpr),
         }
     }
 }
@@ -219,7 +220,7 @@ impl TypeCheck<Box<dyn operator::OperatorBuilder + Send + Sync>>
 
                         error_builder
                             .report_error_for(&e)
-                            .with_code_range(expr.start_pos, expr.end_pos, "This is constant")
+                            .with_code_range(expr.range, "This is constant")
                             .with_resolution("Perhaps you meant to compare a field to this value?")
                             .with_resolution(format!("example: where field1 == {}", constant))
                             .send_report();


Test patch:
diff --git a/tests/integration.rs b/tests/integration.rs
index ae738bb..ba637cd 100644
--- a/tests/integration.rs
+++ b/tests/integration.rs
@@ -12,6 +12,7 @@ struct TestDefinition {
     error: Option<String>,
     notes: Option<String>,
     succeeds: Option<bool>,
+    enabled: Option<bool>,
 }
 
 #[cfg(test)]
@@ -46,6 +47,11 @@ mod integration {
         let contents = fs::read_to_string(path).unwrap();
         let conf: TestDefinition = toml::from_str(&contents).unwrap();
         let err = conf.error.unwrap_or("".to_string());
+
+        if !conf.enabled.unwrap_or(true) {
+            return;
+        }
+
         let asserter = run()
             .env("RUST_BACKTRACE", "0")
             .write_stdin(conf.input)
diff --git a/tests/structured_tests/parse_error_double_from.toml b/tests/structured_tests/parse_error_double_from.toml
index 3b76854..a9a60c6 100644
--- a/tests/structured_tests/parse_error_double_from.toml
+++ b/tests/structured_tests/parse_error_double_from.toml
@@ -8,7 +8,7 @@ error = """
 error: Two `from` clauses were provided
   |
 1 | * | json | parse "a * b" from col as x from col
-  |                          ^^^^^^^^^
+  |                          ^^^^^^^^
   |                                        ^^^^^^^^
   |
   = help: Only one from clause is allowed
diff --git a/tests/structured_tests/parse_error_missing_operand.toml b/tests/structured_tests/parse_error_missing_operand.toml
index 3e507cf..34f6741 100644
--- a/tests/structured_tests/parse_error_missing_operand.toml
+++ b/tests/structured_tests/parse_error_missing_operand.toml
@@ -8,7 +8,7 @@ error = """
 error: expecting an operand for binary operator
   |
 1 | * | where response_ms *
-  |                       ^
+  |                       ^ dangling binary operator
   |
   = help: Add the operand or delete the operator
 Error: Failed to parse query