// BQL subset parser.  Supported grammar:
//
//   SELECT  [, ]* [FROM postings|transactions|balances]
//     [WHERE ] [GROUP BY  [, *]]
//     [ORDER BY  [ASC|DESC] [, ...]] [LIMIT ]
//   BALANCES
//   JOURNAL  [LIMIT ]
//
// Expressions are columns, literals, and the functions year(), month(),
// root(), parent(), leaf() and the aggregate sum().

///|
priv enum Expr {
  EPredicate(Cond)
  EValue(QueryValue)
  ECol(String)
  EStr(String)
  ENum(@decimal.Decimal)
  EDate(@ast.Date)
  ENull
  EStar
  EBool(Bool)
  ECall(String, Array[Expr])
  EBinary(Expr, String, Expr)
}

///|
priv enum Cond {
  CTrue
  CNot(Cond)
  CAnd(Cond, Cond)
  COr(Cond, Cond)
  CCmp(Expr, String, Expr)
  CIsNull(Expr, Bool)
  CTruthy(Expr)
  CRegex(Expr, String, @string.Regex)
}

///|
priv struct OrderSpec {
  expr : Expr
  descending : Bool
}

///|
priv enum QuerySource {
  SrcPostings
  SrcTransactions
  SrcBalances
  SrcDirectives(String)
  SrcClose(@ast.Date)
  SrcOpen(@ast.Date, @ast.Date?)
  SrcRelation(SelectQuery)
  SrcFrom(Cond, @ast.Date?, @ast.Date?, Bool, Bool)
}

///|
priv struct SelectQuery {
  projections : Array[Expr]
  names : Array[String]
  labels : Array[String]
  pivot : (Int32, Int32)?
  distinct : Bool
  source : QuerySource
  filter : Cond
  having : Cond
  groups : Array[Expr]
  orders : Array[OrderSpec]
  limit : Int32?
}

///|
priv enum ParsedQuery {
  PqSelect(SelectQuery)
  PqPrint(Cond, QuerySource)
  PqJournal(String, Int32?)
  PqError(String)
}

///|
priv struct QueryParser {
  tokens : Array[BTok]
  text : String
  positions : Array[Int32]
  mut index : Int32
  mut error : String?
}

///|
fn QueryParser::new(
  tokens : Array[BTok],
  text~ : String = "",
  positions~ : Array[Int32] = [],
) -> QueryParser {
  { tokens, text, positions, index: 0, error: None, }
}

///|
fn QueryParser::peek(self : QueryParser) -> BTok {
  if self.index < self.tokens.length() {
    self.tokens[self.index]
  } else {
    BEof
  }
}

///|
fn QueryParser::advance(self : QueryParser) -> BTok {
  let token = self.peek()
  if self.index < self.tokens.length() {
    self.index = self.index + 1
  }
  token
}

///|
fn QueryParser::at_end(self : QueryParser) -> Bool {
  self.peek() is BEof
}

///|
fn QueryParser::fail(self : QueryParser, message : String) -> Unit {
  match self.error {
    Some(_) => ()
    None => self.error = Some(message)
  }
}

///|
fn QueryParser::peek_word(self : QueryParser) -> String? {
  match self.peek() {
    BWord(word) => Some(word)
    _ => None
  }
}

///|
fn QueryParser::peek_is_keyword(self : QueryParser, keyword : String) -> Bool {
  match self.peek_word() {
    Some(word) => word.to_lower() == keyword
    None => false
  }
}

///|
fn QueryParser::match_keyword(self : QueryParser, keyword : String) -> Bool {
  if self.peek_is_keyword(keyword) {
    self.advance() |> ignore
    true
  } else {
    false
  }
}

///|
fn QueryParser::expect_keyword(self : QueryParser, keyword : String) -> Bool {
  if self.match_keyword(keyword) {
    true
  } else {
    self.fail("expected `" + keyword + "`")
    false
  }
}

///|
fn QueryParser::take_word(self : QueryParser) -> String? {
  match self.peek() {
    BWord(word) => {
      self.advance() |> ignore
      Some(word)
    }
    _ => None
  }
}

///|
fn QueryParser::peek_sym(self : QueryParser) -> String? {
  match self.peek() {
    BSym(symbol) => Some(symbol)
    _ => None
  }
}

///|
fn QueryParser::match_sym(self : QueryParser, symbol : String) -> Bool {
  if self.peek_sym() == Some(symbol) {
    self.advance() |> ignore
    true
  } else {
    false
  }
}

///|
fn QueryParser::expect_sym(self : QueryParser, symbol : String) -> Bool {
  if self.match_sym(symbol) {
    true
  } else {
    self.fail("expected `" + symbol + "`")
    false
  }
}

///|
fn QueryParser::take_int(self : QueryParser) -> Int32? {
  match self.peek() {
    BInt(value) => {
      self.advance() |> ignore
      Some(value)
    }
    BNum(_) => {
      self.fail("expected an integer")
      None
    }
    _ => {
      self.fail("expected an integer")
      None
    }
  }
}

///|
fn parse_primary(parser : QueryParser) -> Expr? {
  match parser.peek() {
    BWord(word) => {
      parser.advance() |> ignore
      let lower = word.to_lower()
      if parser.match_sym("(") {
        if lower == "count" && parser.match_keyword("distinct") {
          parser.fail("DISTINCT is not valid in COUNT arguments")
          return None
        }
        let args : Array[Expr] = []
        if !parser.match_sym(")") {
          let mut done = false
          while !done {
            if parser.match_sym("*") {
              if lower != "count" {
                parser.fail("unexpected star")
                return None
              }
              args.push(EStar)
            } else {
              match parse_expr(parser) {
                Some(expr) => args.push(expr)
                None => return None
              }
            }
            if parser.match_sym(",") {
              continue
            }
            done = true
          }
          if !parser.expect_sym(")") {
            return None
          }
        }
        if ["sum", "count", "first", "last", "min", "max"].contains(lower) &&
          args.any(contains_sum) {
          parser.fail("compilation query.invalid_aggregate")
          return None
        }
        if lower == "coalesce" {
          if args.is_empty() {
            parser.fail(
              "query.oracle_type_error: IndexError coalesce arguments",
            )
            return None
          }
          for arg in args {
            if constant_query_error(arg) is Some(error) {
              parser.fail(error)
              return None
            }
          }
        }
        if lower == "coalesce" || oracle_signatures.any(fn(s) { s.0 == lower }) {
          Some(ECall(lower, args))
        } else {
          parser.fail(
            "compilation query.invalid_signature: unknown function `" +
            word +
            "`",
          )
          None
        }
      } else if lower == "null" {
        Some(ENull)
      } else if lower == "true" {
        Some(EBool(true))
      } else if lower == "false" {
        Some(EBool(false))
      } else {
        match lower {
          "date"
          | "flag"
          | "payee"
          | "narration"
          | "account"
          | "root"
          | "parent"
          | "leaf"
          | "units"
          | "currency"
          | "cost"
          | "value"
          | "position"
          | "tags"
          | "links"
          | "accounts"
          | "weight"
          | "posting_flag"
          | "other_accounts"
          | "number"
          | "cost_number"
          | "cost_currency"
          | "cost_date"
          | "cost_label"
          | "price"
          | "balance"
          | "entry"
          | "location"
          | "type"
          | "id"
          | "filename"
          | "lineno"
          | "year"
          | "month"
          | "day"
          | "name"
          | "amount"
          | "tolerance"
          | "discrepancy"
          | "description"
          | "comment"
          | "meta"
          | "open"
          | "close" => Some(ECol(lower))
          _ => Some(ECol(lower))
        }
      }
    }
    BStr(value) => {
      let quoted = !parser.positions.is_empty() &&
        parser.text.get_char(parser.positions[parser.index]) == Some('"')
      parser.advance() |> ignore
      Some(if quoted { ECall("__quoted", [EStr(value)]) } else { EStr(value) })
    }
    BInt(value) => {
      parser.advance() |> ignore
      Some(ENum(@decimal.Decimal::from_int(value)))
    }
    BNum(number) => {
      parser.advance() |> ignore
      Some(ENum(number))
    }
    BDate(date) => {
      parser.advance() |> ignore
      Some(EDate(date))
    }
    BSym(symbol) =>
      if symbol == "(" {
        parser.advance() |> ignore
        let first = match parse_expr(parser) {
          Some(e) => e
          None => return None
        }
        if parser.match_sym(",") {
          let values : Array[QueryValue] = []
          let mut expr = first
          let mut first_item = true
          while true {
            match literal_value(expr) {
              Some(v) => if first_item || !(v is QNull) { values.push(v) }
              None => {
                parser.fail("list elements must be literals")
                return None
              }
            }
            first_item = false
            if parser.match_sym(")") {
              return Some(EValue(QList(values)))
            }
            expr = match parse_expr(parser) {
              Some(e) => e
              None => return None
            }
            if !parser.match_sym(",") {
              if !parser.expect_sym(")") {
                return None
              }
              match literal_value(expr) {
                Some(v) => if !(v is QNull) { values.push(v) }
                None => {
                  parser.fail("list elements must be literals")
                  return None
                }
              }
              return Some(EValue(QList(values)))
            }
          }
        }
        if !parser.expect_sym(")") {
          return None
        }
        Some(first)
      } else if symbol == "-" || symbol == "+" {
        parser.advance() |> ignore
        match parse_primary(parser) {
          Some(expr) =>
            Some(EBinary(ENum(@decimal.Decimal::zero()), symbol, expr))
          None => None
        }
      } else if symbol == "*" {
        parser.advance() |> ignore
        parser.fail("unexpected star")
        None
      } else {
        parser.fail("expected an expression")
        None
      }
    BEof => {
      parser.fail("unexpected end of query")
      None
    }
  }
}

///|
fn parse_binary(parser : QueryParser, min_prec : Int32) -> Expr? {
  let mut left = match parse_primary(parser) {
    Some(e) => e
    None => return None
  }
  while true {
    let op = match parser.peek_sym() {
      Some(s) => s
      None => break
    }
    let prec = match op {
      "+" | "-" => 1
      "*" | "/" | "%" => 2
      _ => 0
    }
    if prec < min_prec || prec == 0 {
      break
    }
    parser.advance() |> ignore
    let right = match parse_binary(parser, prec + 1) {
      Some(e) => e
      None => return None
    }
    left = EBinary(left, op, right)
  }
  Some(left)
}

///|
fn parse_expr(parser : QueryParser) -> Expr? {
  match parse_cond_or(parser) {
    Some(CTruthy(expr)) => Some(expr)
    Some(cond) => Some(EPredicate(cond))
    None => None
  }
}

///|
fn parse_cond_atom(parser : QueryParser) -> Cond? {
  match parse_binary(parser, 1) {
    Some(left) =>
      if parser.match_keyword("between") {
        let low = match parse_binary(parser, 1) {
          Some(e) => e
          None => return None
        }
        if !parser.expect_keyword("and") {
          return None
        }
        let high = match parse_binary(parser, 1) {
          Some(e) => e
          None => return None
        }
        Some(CTruthy(ECall("__between", [left, low, high])))
      } else if parser.match_keyword("in") {
        parse_binary(parser, 1).map(fn(right) {
          CTruthy(ECall("__in", [left, right]))
        })
      } else if parser.peek() is BWord(word) &&
        word.to_lower() == "not" &&
        parser.tokens.get(parser.index + 1) is Some(BWord(next)) &&
        next.to_lower() == "in" {
        parser.advance() |> ignore
        parser.advance() |> ignore
        parse_binary(parser, 1).map(fn(right) {
          CTruthy(ECall("__notin", [left, right]))
        })
      } else if parser.match_sym("~") {
        match parser.advance() {
          BStr(pattern) => {
            let regex = @string.Regex(
              "(?i:" + scoped_case_pattern(pattern) + ")",
            ) catch {
              _ => {
                parser.fail(
                  "unsupported query.unsupported.regex: invalid or unsupported regular expression",
                )
                return None
              }
            }
            Some(CRegex(left, pattern, regex))
          }
          _ => {
            parser.fail("regex requires a literal pattern")
            None
          }
        }
      } else if parser.match_keyword("is") {
        let negated = parser.match_keyword("not")
        if parser.expect_keyword("null") {
          Some(CIsNull(left, negated))
        } else {
          None
        }
      } else {
        match parser.peek_sym() {
          Some(symbol) =>
            if symbol == "=" ||
              symbol == "==" ||
              symbol == "!=" ||
              symbol == "<>" ||
              symbol == "<" ||
              symbol == "<=" ||
              symbol == ">" ||
              symbol == ">=" {
              parser.advance() |> ignore
              match parse_binary(parser, 1) {
                Some(right) => Some(CCmp(left, symbol, right))
                None => None
              }
            } else {
              Some(CTruthy(left))
            }
          None => Some(CTruthy(left))
        }
      }
    None => None
  }
}

///|
fn parse_cond_not(parser : QueryParser) -> Cond? {
  if parser.match_keyword("not") {
    match parse_cond_not(parser) {
      Some(cond) => Some(CNot(cond))
      None => None
    }
  } else {
    parse_cond_atom(parser)
  }
}

///|
fn parse_cond_and(parser : QueryParser) -> Cond? {
  match parse_cond_not(parser) {
    Some(left) => {
      let mut cond = left
      while parser.match_keyword("and") {
        match parse_cond_not(parser) {
          Some(right) => cond = CAnd(cond, right)
          None => return None
        }
      }
      Some(cond)
    }
    None => None
  }
}

///|
fn parse_cond_or(parser : QueryParser) -> Cond? {
  match parse_cond_and(parser) {
    Some(left) => {
      let mut cond = left
      while parser.match_keyword("or") {
        match parse_cond_and(parser) {
          Some(right) => cond = COr(cond, right)
          None => return None
        }
      }
      Some(cond)
    }
    None => None
  }
}

///|
fn parse_expr_list(parser : QueryParser) -> Array[Expr]? {
  let items : Array[Expr] = []
  match parse_expr(parser) {
    Some(expr) => items.push(expr)
    None => return None
  }
  while parser.match_sym(",") {
    match parse_expr(parser) {
      Some(expr) => items.push(expr)
      None => return None
    }
  }
  Some(items)
}

///|
fn parse_source_base(parser : QueryParser) -> QuerySource? {
  if parser.match_sym("(") {
    if !parser.expect_keyword("select") {
      return None
    }
    match parse_select(parser) {
      PqSelect(query) => {
        if !parser.expect_sym(")") {
          return None
        }
        return Some(SrcRelation(query))
      }
      PqError(message) => {
        parser.fail(message)
        return None
      }
      _ => return None
    }
  }
  let hashed = parser.match_sym("#")
  match parser.take_word() {
    Some(word) => {
      if hashed &&
        ![
          "postings", "transactions", "entries", "accounts", "balances", "commodities",
          "documents", "events", "notes", "prices",
        ].contains(word) {
        parser.fail("compilation query.invalid_source `#" + word + "`")
        return None
      }
      match word.to_lower() {
        "postings" => Some(SrcPostings)
        "transactions" => Some(SrcTransactions)
        "balances" =>
          if hashed {
            Some(SrcDirectives("balances"))
          } else {
            Some(SrcBalances)
          }
        "entries"
        | "accounts"
        | "commodities"
        | "documents"
        | "events"
        | "notes"
        | "prices" => Some(SrcDirectives(word.to_lower()))
        _ => {
          parser.fail("unknown FROM source `" + word + "`")
          None
        }
      }
    }
    None => {
      parser.fail("expected a FROM source")
      None
    }
  }
}

///|
fn parse_select(parser : QueryParser) -> ParsedQuery {
  let distinct = parser.match_keyword("distinct")
  let star = parser.match_sym("*")
  let projections : Array[Expr] = []
  let aliases : Array[String?] = []
  let expressions : Array[String] = []
  if !star {
    while true {
      let start = parser.index
      match parse_expr(parser) {
        Some(expr) => projections.push(expr)
        None => return PqError(parser.error.unwrap_or("expected a projection"))
      }
      expressions.push(
        if projections.last().unwrap() is ECol(name) {
          name
        } else if parser.positions.is_empty() {
          oracle_expr_label(projections.last().unwrap())
        } else {
          parser.text[parser.positions[start]:parser.positions[parser.index]]
          .trim()
          .to_owned()
        },
      )
      let column_alias = if parser.match_keyword("as") {
        match parser.advance() {
          BWord(name) => Some(name.to_lower())
          BStr(name) => {
            if parser.positions.is_empty() ||
              parser.text.get_char(parser.positions[parser.index - 1]) !=
              Some('"') {
              return PqError("expected a column alias")
            }
            Some(name)
          }
          _ => return PqError("expected a column alias")
        }
      } else {
        None
      }
      aliases.push(column_alias)
      if !parser.match_sym(",") {
        break
      }
    }
  }
  let mut source = SrcPostings
  if parser.match_keyword("from") {
    match parse_source(parser) {
      Some(found) => source = found
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid FROM clause"
          },
        )
    }
  }
  if star {
    let names = if source is SrcRelation(inner) {
      inner.names
    } else if source is SrcDirectives(table) {
      table_star(table)
    } else if source is SrcTransactions {
      ["date", "flag", "payee", "narration", "tags", "links", "accounts"]
    } else {
      ["date", "flag", "payee", "narration", "position"]
    }
    for name in names {
      projections.push(ECol(name))
      aliases.push(None)
      expressions.push(name)
    }
  }
  let names = projections.mapi(fn(i, _) { aliases[i].unwrap_or(expressions[i]) })
  let labels = projections.mapi(fn(i, e) {
    aliases[i].unwrap_or(
      if expr_any(e, fn(e) { e is ECall("__quoted", _) }) {
        expressions[i]
      } else {
        expr_label(e)
      },
    )
  })
  let mut filter = CTrue
  if parser.match_keyword("where") {
    match parse_cond_or(parser) {
      Some(cond) => filter = cond
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid WHERE clause"
          },
        )
    }
  }
  let groups : Array[Expr] = []
  if parser.match_keyword("group") {
    if !parser.expect_keyword("by") {
      return PqError(
        match parser.error {
          Some(message) => message
          None => "expected BY"
        },
      )
    }
    match parse_expr_list(parser) {
      Some(items) =>
        for item in items {
          groups.push(
            resolve_reference(parser, item, projections, names, "GROUP-BY"),
          )
        }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid GROUP BY clause"
          },
        )
    }
  }
  let mut having = CTrue
  let has_having = parser.match_keyword("having")
  if has_having {
    match parse_cond_or(parser) {
      Some(cond) => having = cond
      None => return PqError("invalid HAVING clause")
    }
  }
  let orders : Array[OrderSpec] = []
  if parser.match_keyword("order") {
    if !parser.expect_keyword("by") {
      return PqError(
        match parser.error {
          Some(message) => message
          None => "expected BY"
        },
      )
    }
    let mut more = true
    while more {
      match parse_expr(parser) {
        Some(expr) => {
          let mut descending = false
          if parser.match_keyword("desc") {
            descending = true
          } else {
            parser.match_keyword("asc") |> ignore
          }
          let expr = resolve_reference(
            parser, expr, projections, names, "ORDER-BY",
          )
          orders.push({ expr, descending, })
        }
        None =>
          return PqError(
            match parser.error {
              Some(message) => message
              None => "invalid ORDER BY clause"
            },
          )
      }
      more = parser.match_sym(",")
    }
  }
  let mut limit = None
  let mut pivot = None
  if parser.match_keyword("pivot") {
    if !parser.expect_keyword("by") {
      return PqError("expected BY")
    }
    let first = parse_pivot_reference(parser, projections, names)
    if !parser.expect_sym(",") {
      return PqError("PIVOT BY requires two columns")
    }
    let second = parse_pivot_reference(parser, projections, names)
    if first == second && parser.error is None {
      return PqError(
        "compilation query.invalid_reference: the two PIVOT BY columns cannot be the same column",
      )
    }
    pivot = Some((first, second))
  }
  if parser.match_keyword("limit") {
    match parser.take_int() {
      Some(value) => {
        if value < 0 {
          return PqError("LIMIT must be non-negative")
        }
        limit = Some(value)
      }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid LIMIT"
          },
        )
    }
  }
  if !parser.at_end() && parser.peek_sym() != Some(")") {
    return PqError("unexpected input after query")
  }
  if parser.error is Some(message) {
    return PqError(message)
  }
  for i in 0.. ignore
  }
  for e in groups {
    expression_type(e, source, errors) |> ignore
  }
  for o in orders {
    expression_type(o.expr, source, errors) |> ignore
  }
  condition_types(filter, source, errors)
  condition_types(having, source, errors)
  if !errors.is_empty() {
    return PqError(errors[0])
  }
  for expr in groups {
    let kind = expression_type(expr, source, [])
    if implicit_groups && (kind == "set" || kind == "frozenset") {
      continue
    }
    if ["dict", "list", "set", "inventory"].contains(kind) {
      return PqError("compilation query.invalid_group: non-hashable " + kind)
    }
    if kind == "frozenset" || kind == "generic_set" {
      return PqError(
        "query.oracle_type_error: TypeError oracle generic group type",
      )
    }
  }
  PqSelect({
    projections,
    names,
    labels,
    pivot,
    distinct,
    source,
    filter,
    having,
    groups,
    orders,
    limit,
  })
}

///|
fn parse_legacy_journal(parser : QueryParser) -> ParsedQuery {
  let account = match parser.peek() {
    BWord(word) => {
      parser.advance() |> ignore
      word
    }
    BStr(value) => {
      parser.advance() |> ignore
      value
    }
    _ => return PqError("JOURNAL expects an account name")
  }
  let mut limit = None
  if parser.match_keyword("limit") {
    match parser.take_int() {
      Some(value) => {
        if value < 0 {
          return PqError("LIMIT must be non-negative")
        }
        limit = Some(value)
      }
      None =>
        return PqError(
          match parser.error {
            Some(message) => message
            None => "invalid LIMIT"
          },
        )
    }
  }
  if !parser.at_end() {
    return PqError("unexpected input after JOURNAL")
  }
  PqJournal(account, limit)
}

///|
fn parse_query(text : String) -> ParsedQuery {
  let unsupported : Array[String] = []
  let positions : Array[Int32] = []
  let tokens = lex_bql(text, unsupported~, positions~)
  if unsupported.length() > 0 {
    return PqError(unsupported[0])
  }
  if tokens.length() > 256 {
    let mut nesting = 0
    let mut operators = 0
    for token in tokens {
      match token {
        BSym("(") => nesting = nesting + 1
        BSym(")") => nesting = nesting - 1
        BSym("+" | "-" | "*" | "/" | "%" | "=" | "<" | ">" | "!=" | "<=" | ">=") =>
          operators = operators + 1
        BWord(w) =>
          if ["not", "and", "or"].contains(w.to_lower()) {
            operators = operators + 1
          }
        _ => ()
      }
      if nesting > 256 || operators > 256 {
        return PqError(
          "syntax query.depth_limit: query exceeds portable recursion budget",
        )
      }
    }
  }
  for i in 0..<(tokens.length() - 1) {
    if tokens[i] is BSym("#") && positions[i + 1] != positions[i] + 1 {
      return PqError(
        "syntax query.invalid_source" +
        bql_lex_error(text, positions[i]).replace(
          old="lexical query.invalid_character",
          new="",
        ),
      )
    }
  }
  let parser = QueryParser::new(tokens, text~, positions~)
  if parser.at_end() {
    return PqError("empty query")
  }
  let result = match parser.take_word() {
    Some(word) =>
      match word.to_lower() {
        "print" => parse_print(parser)
        "select" => parse_select(parser)
        "balances" => parse_balances(parser)
        "journal" => parse_journal(parser)
        _ => PqError("unknown statement `" + word + "`")
      }
    None => PqError("query must start with SELECT, BALANCES or JOURNAL")
  }
  let result = if !(result is PqError(_)) &&
    tokens[0] is BWord(word) &&
    word.to_lower() == "select" &&
    !parser.at_end() {
    PqError("unexpected input after query")
  } else {
    result
  }
  match result {
    PqError(message) => {
      let index = (parser.index - 1).max(0).min(positions.length() - 1)
      let location = bql_lex_error(text, positions[index]).replace(
        old="lexical query.invalid_character",
        new="",
      )
      PqError(message + location)
    }
    _ => result
  }
}

///|
fn parse_balances(parser : QueryParser) -> ParsedQuery {
  let mut mode = ""
  if parser.match_keyword("at") {
    mode = match parser.take_word() {
      Some(word) => word
      None => return PqError("expected valuation function")
    }
  }
  let tokens = [BWord("account"), BSym(","), BWord("sum"), BSym("(")]
  if mode != "" {
    tokens.append([BWord(mode), BSym("(")])
  }
  tokens.push(BWord("position"))
  if mode != "" {
    tokens.push(BSym(")"))
  }
  tokens.push(BSym(")"))
  for i in parser.index..<(parser.tokens.length() - 1) {
    tokens.push(parser.tokens[i])
  }
  tokens.append([BWord("group"), BWord("by"), BWord("account"), BEof])
  match parse_select(QueryParser::new(tokens)) {
    PqSelect(query) => {
      let label = if mode == "" {
        "SUM((position))"
      } else {
        "SUM(" + mode + "(position))"
      }
      PqSelect({
        ..query,
        labels: ["account", label],
        names: ["account", label],
        orders: [
          {
            expr: ECall("account_sortkey", [ECol("account")]),
            descending: false,
          },
        ],
      })
    }
    other => other
  }
}

///|
fn transaction_expr(expr : Expr) -> Bool {
  match expr {
    EPredicate(c) => transaction_cond(c)
    ECol(
      "date"
      | "flag"
      | "payee"
      | "narration"
      | "tags"
      | "links"
      | "accounts"
      | "meta"
    ) => true
    ECol(_) => false
    ECall("sum", _) => false
    ECall(_, args) => args.all(transaction_expr)
    EBinary(a, _, b) => transaction_expr(a) && transaction_expr(b)
    _ => true
  }
}

///|
fn transaction_cond(cond : Cond) -> Bool {
  match cond {
    CTrue => true
    CNot(c) => transaction_cond(c)
    CAnd(a, b) | COr(a, b) => transaction_cond(a) && transaction_cond(b)
    CCmp(a, _, b) => transaction_expr(a) && transaction_expr(b)
    CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) => transaction_expr(e)
  }
}

///|
fn table_columns(table : String) -> Array[String] {
  match table {
    "entries" =>
      [
        "id", "type", "filename", "lineno", "date", "year", "month", "day", "flag",
        "payee", "narration", "description", "tags", "links", "meta", "accounts",
      ]
    "accounts" => ["account", "open", "close"]
    "balances" =>
      ["meta", "date", "account", "amount", "tolerance", "discrepancy"]
    "commodities" => ["meta", "date", "name"]
    "documents" => ["meta", "date", "account", "filename", "tags", "links"]
    "events" => ["meta", "date", "type", "description"]
    "notes" => ["meta", "date", "account", "comment", "tags", "links"]
    "prices" => ["meta", "date", "currency", "amount"]
    _ => []
  }
}

///|
fn table_star(table : String) -> Array[String] {
  match table {
    "entries" => table_columns(table)
    "prices" => ["date", "currency", "amount"]
    "events" => ["date", "type", "description"]
    "documents" => ["date", "account", "filename", "tags", "links"]
    "balances" => ["date", "account", "amount", "tolerance", "discrepancy"]
    "notes" => ["date", "account", "comment", "tags", "links"]
    _ => table_columns(table)
  }
}

///|
fn table_expr(expr : Expr, table : String) -> Bool {
  match expr {
    EPredicate(c) => table_cond(c, table)
    ECol(name) => table_columns(table).contains(name)
    ECall(_, args) => args.all(fn(arg) { table_expr(arg, table) })
    EBinary(a, _, b) => table_expr(a, table) && table_expr(b, table)
    _ => true
  }
}

///|
fn table_cond(cond : Cond, table : String) -> Bool {
  match cond {
    CTrue => true
    CNot(c) => table_cond(c, table)
    CAnd(a, b) | COr(a, b) => table_cond(a, table) && table_cond(b, table)
    CCmp(a, _, b) => table_expr(a, table) && table_expr(b, table)
    CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) => table_expr(e, table)
  }
}

///|
fn posting_expr(expr : Expr) -> Bool {
  match expr {
    EPredicate(c) => posting_cond(c)
    ECol(
      "name"
      | "amount"
      | "tolerance"
      | "discrepancy"
      | "comment"
      | "open"
      | "close"
    ) => false
    ECall(_, args) => args.all(posting_expr)
    EBinary(a, _, b) => posting_expr(a) && posting_expr(b)
    _ => true
  }
}

///|
fn posting_cond(cond : Cond) -> Bool {
  match cond {
    CTrue => true
    CNot(c) => posting_cond(c)
    CAnd(a, b) | COr(a, b) => posting_cond(a) && posting_cond(b)
    CCmp(a, _, b) => posting_expr(a) && posting_expr(b)
    CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) => posting_expr(e)
  }
}

///|
fn aggregate_free_cond(cond : Cond) -> Bool {
  match cond {
    CTrue => true
    CNot(c) => aggregate_free_cond(c)
    CAnd(a, b) | COr(a, b) => aggregate_free_cond(a) && aggregate_free_cond(b)
    CCmp(a, _, b) => !contains_sum(a) && !contains_sum(b)
    CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) => !contains_sum(e)
  }
}

///|
fn unaggregated_column(expr : Expr) -> Bool {
  match expr {
    EPredicate(c) => unaggregated_condition(c)
    ECol(_) => true
    ECall("sum" | "count" | "first" | "last" | "min" | "max", _) => false
    ECall(_, args) => args.any(unaggregated_column)
    EBinary(a, _, b) => unaggregated_column(a) || unaggregated_column(b)
    _ => false
  }
}

///|
fn same_expr(a : Expr, b : Expr) -> Bool {
  match (a, b) {
    (EPredicate(a), EPredicate(b)) => same_condition(a, b)
    (ECol(a), ECol(b)) | (EStr(a), EStr(b)) => a == b
    (EValue(a), EValue(b)) => a.to_json() == b.to_json()
    (ENum(a), ENum(b)) => a.to_string() == b.to_string()
    (EDate(a), EDate(b)) => a.to_string() == b.to_string()
    (EBool(a), EBool(b)) => a == b
    (ENull, ENull) | (EStar, EStar) => true
    (EBinary(a, op, b), EBinary(c, other, d)) =>
      op == other && same_expr(a, c) && same_expr(b, d)
    (ECall(name, args), ECall(other, values)) =>
      name == other &&
      args.length() == values.length() &&
      args.iter().zip(values.iter()).all(fn(pair) { same_expr(pair.0, pair.1) })
    _ => false
  }
}

///|
fn unaggregated_condition(cond : Cond) -> Bool {
  match cond {
    CTrue => false
    CNot(c) => unaggregated_condition(c)
    CAnd(a, b) | COr(a, b) =>
      unaggregated_condition(a) || unaggregated_condition(b)
    CCmp(a, _, b) => unaggregated_column(a) || unaggregated_column(b)
    CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) => unaggregated_column(e)
  }
}

///|
fn parse_print(parser : QueryParser) -> ParsedQuery {
  let mut filter = CTrue
  let mut period = SrcDirectives("entries")
  if parser.match_keyword("from") {
    if !(parser.peek() is BWord(word) &&
      ["open", "close", "clear"].contains(word.to_lower())) {
      filter = match parse_cond_or(parser) {
        Some(c) => c
        None => return PqError(parser.error.unwrap_or("invalid PRINT filter"))
      }
    }
    if parser.peek() is BWord(word) &&
      ["open", "close", "clear"].contains(word.to_lower()) {
      period = match parse_source(parser) {
        Some(s) => s
        None => {
          let error = parser.error.unwrap_or("invalid PRINT period")
          return PqError(
            if error == "OPEN ON requires a date after 0001-01-01" {
              "query.oracle_type_error: OverflowError date value out of range"
            } else {
              error
            },
          )
        }
      }
    }
  }
  if !parser.at_end() {
    return PqError("unexpected input after PRINT")
  }
  let errors : Array[String] = []
  if !aggregate_free_cond(filter) {
    return PqError("compilation query.invalid_aggregate: FROM clause")
  }
  filter = resolve_quoted_condition(filter, SrcDirectives("entries"))
  projection_condition_types(filter, SrcDirectives("entries"), errors)
  if !errors.is_empty() {
    return PqError(errors[0])
  }
  PqPrint(filter, period)
}

///|
fn parse_source(parser : QueryParser) -> QuerySource? {
  if parser.peek_sym() == Some("#") ||
    parser.peek_sym() == Some("(") ||
    (
      parser.peek() is BWord(word) &&
      [
        "postings", "transactions", "balances", "entries", "commodities", "documents",
        "events", "notes", "prices",
      ].contains(word.to_lower())
    ) {
    return parse_source_base(parser)
  }
  let mut filter = CTrue
  if !(parser.peek() is BWord(word) &&
    ["open", "close", "clear"].contains(word.to_lower())) {
    filter = match parse_cond_or(parser) {
      Some(c) => c
      None => return None
    }
  }
  let mut start = None
  let mut end = None
  let mut close_all = false
  if parser.match_keyword("open") {
    if !parser.expect_keyword("on") {
      return None
    }
    start = match parser.advance() {
      BDate(d) => {
        if ordinal(d) == 1 {
          parser.fail(
            "query.oracle_type_error: OverflowError opening date out of range",
          )
          return None
        }
        Some(d)
      }
      _ => {
        parser.fail("OPEN ON requires a date")
        return None
      }
    }
  }
  if parser.match_keyword("close") {
    if parser.match_keyword("on") {
      end = match parser.advance() {
        BDate(d) => Some(d)
        _ => {
          parser.fail("CLOSE ON requires a date")
          return None
        }
      }
    } else {
      close_all = true
    }
  }
  let clear = parser.match_keyword("clear")
  if start is Some(_) && close_all && clear {
    parser.fail("query.oracle_type_error: TypeError OPEN CLOSE CLEAR end date")
    return None
  }
  let errors : Array[String] = []
  filter = resolve_quoted_condition(filter, SrcPostings)
  projection_condition_types(filter, SrcPostings, errors)
  if !aggregate_free_cond(filter) {
    errors.push("compilation query.invalid_aggregate: FROM clause")
  }
  if !errors.is_empty() {
    parser.fail(errors[0])
    return None
  }
  Some(SrcFrom(filter, start, end, close_all, clear))
}

///|
fn parse_journal(parser : QueryParser) -> ParsedQuery {
  if parser.peek() is BWord(word) &&
    !["at", "from"].contains(word.to_lower()) &&
    word.to_lower() != "limit" {
    return parse_legacy_journal(parser)
  }
  if parser.peek() is BStr(_) &&
    parser.tokens.get(parser.index + 1) is Some(BWord(word)) &&
    word.to_lower() == "limit" {
    return parse_legacy_journal(parser)
  }
  let account = match parser.peek() {
    BStr(pattern) => {
      parser.advance() |> ignore
      Some(pattern)
    }
    _ => None
  }
  let mut mode = ""
  if parser.match_keyword("at") {
    mode = match parser.take_word() {
      Some(s) => s
      None => return PqError("expected valuation function")
    }
  }
  let source = if parser.match_keyword("from") {
    match parse_source(parser) {
      Some(s) => s
      None => return PqError(parser.error.unwrap_or("invalid FROM clause"))
    }
  } else {
    SrcPostings
  }
  if !parser.at_end() {
    return PqError("unexpected input after JOURNAL")
  }
  let filter = match account {
    Some(pattern) => {
      let regex = @string.Regex("(?i:" + scoped_case_pattern(pattern) + ")") catch {
        _ =>
          return PqError(
            "query.oracle_type_error: PatternError invalid journal regex",
          )
      }
      CRegex(ECol("account"), pattern, regex)
    }
    None => CTrue
  }
  let valuation = fn(column : String) {
    if mode == "" {
      ECol(column)
    } else {
      ECall(mode.to_lower(), [ECol(column)])
    }
  }
  let projections = [
    ECol("date"),
    ECol("flag"),
    ECall("maxwidth", [ECol("payee"), ENum(@decimal.Decimal::from_int(48))]),
    ECall("maxwidth", [ECol("narration"), ENum(@decimal.Decimal::from_int(80))]),
    ECol("account"),
    valuation("position"),
    valuation("balance"),
  ]
  let errors : Array[String] = []
  for expr in projections {
    expression_type(expr, source, errors) |> ignore
  }
  if !errors.is_empty() {
    return PqError(errors[0])
  }
  let labels = [
    "date",
    "flag",
    "MAXWIDTH(payee, 48)",
    "MAXWIDTH(narration, 80)",
    "account",
    if mode == "" {
      "position"
    } else {
      mode + "(position)"
    },
    if mode == "" {
      "balance"
    } else {
      mode + "(balance)"
    },
  ]
  PqSelect({
    projections,
    names: labels,
    labels,
    source,
    filter,
    groups: [],
    orders: [],
    limit: None,
    having: CTrue,
    distinct: false,
    pivot: None,
  })
}