// BQL evaluation over a realized ledger.  The engine works on a flat posting
// table (or an inventory-position table for FROM balances), supports a WHERE
// predicate, grouping with sum()/count(), ordering and LIMIT.

///|
priv struct BqlRow {
  date : @ast.Date
  mut extra : RowExtra?
  flag : String
  payee : String?
  narration : String
  tags : Array[String]
  links : Array[String]
  meta : @ast.Meta
  entry_meta : @ast.Meta
  accounts : Array[@ast.Posting]
  account : String
  units : @ast.Amount
  cost : @ast.Cost?
  value : @ast.Amount?
  position : @booking.Position
}

///|
fn posting_cost(posting : @ast.Posting, units : @ast.Amount) -> @ast.Cost? {
  match posting.cost {
    Some(spec) => {
      let currency = match spec.currency {
        Some(name) => name
        None => units.currency
      }
      let number = match spec.number_per {
        Some(per) => Some(per)
        None =>
          match spec.number_total {
            Some(total) =>
              if units.number.is_zero() {
                None
              } else {
                total.div(units.number)
              }
            None => None
          }
      }
      match number {
        Some(value) =>
          Some(@ast.Cost::new(value, currency, spec.date, spec.label))
        None => None
      }
    }
    None => None
  }
}

///|
fn posting_value(
  units : @ast.Amount,
  cost : @ast.Cost?,
  price : @ast.PriceAnno?,
) -> @ast.Amount? {
  match cost {
    Some(c) => Some(@ast.Amount::new(units.number.mul(c.number), c.currency))
    None =>
      match price {
        Some(@ast.PerUnit(amount)) =>
          Some(
            @ast.Amount::new(units.number.mul(amount.number), amount.currency),
          )
        Some(@ast.Total(amount)) =>
          Some(
            @ast.Amount::new(
              if units.number.is_negative() {
                amount.number.abs().neg()
              } else {
                amount.number.abs()
              },
              amount.currency,
            ),
          )
        None => None
      }
  }
}

///|
fn last_entry_date(realized : @realization.RealizedLedger) -> @ast.Date? {
  let mut latest : @ast.Date? = None
  for entry in realized.entries {
    latest = match latest {
      Some(date) =>
        if date.less_than(entry.date) {
          Some(entry.date)
        } else {
          Some(date)
        }
      None => Some(entry.date)
    }
  }
  latest
}

///|
fn posting_rows(
  realized : @realization.RealizedLedger,
  origin : Bool,
) -> Array[BqlRow] {
  let rows : Array[BqlRow] = []
  for entry in realized.entries {
    match entry.directive {
      DTransaction(txn) =>
        for posting in booked_postings(txn.postings) {
          match posting.units {
            Some(units) => {
              let cost = posting_cost(posting, units)
              rows.push({
                extra: if origin {
                  Some(new_row_extra(Some(entry), None, Some(posting)))
                } else {
                  None
                },
                date: entry.date,
                flag: txn.flag,
                payee: txn.payee,
                narration: txn.narration,
                tags: txn.tags,
                links: txn.links,
                meta: posting.meta,
                entry_meta: entry.meta,
                accounts: txn.postings,
                account: posting.account,
                units,
                cost,
                value: posting_value(units, cost, posting.price),
                position: @booking.Position::new(units, cost),
              })
            }
            None => ()
          }
        }
      _ => ()
    }
  }
  rows
}

///|
fn balance_rows(realized : @realization.RealizedLedger) -> Array[BqlRow] {
  let rows : Array[BqlRow] = []
  let date = match realized.at {
    Some(at) => at
    None =>
      match last_entry_date(realized) {
        Some(latest) => latest
        None => @ast.Date::new(1970, 1, 1).unwrap()
      }
  }
  for account in realized.account_names() {
    for position in realized.inventory(account).positions {
      rows.push({
        extra: None,
        date,
        flag: "",
        payee: None,
        narration: "",
        tags: [],
        links: [],
        meta: @ast.Meta::new(),
        entry_meta: @ast.Meta::new(),
        accounts: [],
        account,
        units: position.units,
        cost: position.cost,
        value: posting_value(position.units, position.cost, None),
        position,
      })
    }
  }
  rows
}

///|
fn source_rows(
  realized : @realization.RealizedLedger,
  source : QuerySource,
  origin : Bool,
) -> Array[BqlRow] {
  match source {
    SrcPostings | SrcFrom(_, _, _, _, _) => posting_rows(realized, origin)
    SrcTransactions => transaction_rows(realized, origin)
    SrcBalances => balance_rows(realized)
    SrcDirectives(table) => directive_rows(realized, table)
    SrcOpen(start, close) => opening_rows(realized, start, close)
    SrcClose(date) =>
      posting_rows(realized, origin).filter(fn(row) { row.date.less_than(date) })
    SrcRelation(_) => [] // Materialized by run_select, with error propagation.
  }
}

///|
fn eval_column(ctx : QueryContext, row : BqlRow, name : String) -> QueryValue {
  if row.extra is Some(extra) && extra.cells is Some(cells) {
    return cells.get(name).unwrap_or(QNull)
  }
  if row.extra is Some(extra) &&
    extra.entry is Some(entry) &&
    extra.source is Some(table) {
    return directive_column(ctx, entry, name, table)
  }
  match name {
    "balance" => running_balance(ctx, row)
    "entry" | "id" =>
      match row_entry(row) {
        Some(entry) => {
          let value = directive_value(entry)
          if name == "id" {
            QText(directive_identity(value))
          } else {
            value
          }
        }
        None => {
          query_error(ctx, "unsupported query.unsupported.synthetic_origin")
          QNull
        }
      }
    "meta" =>
      if row_posting(row) is Some(p) {
        posting_metadata(p)
      } else {
        match row_entry(row) {
          Some(entry) => entry_metadata(entry)
          None => {
            query_error(ctx, "unsupported query.unsupported.synthetic_metadata")
            QNull
          }
        }
      }
    "type" => QText("transaction")
    "filename" | "lineno" | "location" =>
      match row_entry(row) {
        Some(e) =>
          if row_posting(row) is Some(p) && p.loc.file == "" && p.loc.line == 0 {
            QNull
          } else if name == "filename" {
            QText(
              row_posting(row)
              .map(fn(p) { p.loc.file })
              .unwrap_or(query_location(e).file),
            )
          } else if name == "lineno" {
            QInt(
              @bigint.BigInt::from_int(
                row_posting(row)
                .map(fn(p) { p.loc.line })
                .unwrap_or(query_location(e).line),
              ),
            )
          } else {
            QText(
              row_posting(row)
              .map(fn(p) { p.loc.file + ":" + p.loc.line.to_string() + ":" })
              .unwrap_or(
                query_location(e).file +
                ":" +
                query_location(e).line.to_string() +
                ":",
              ),
            )
          }
        None => {
          query_error(ctx, "unsupported query.unsupported.synthetic_origin")
          QNull
        }
      }
    "posting_flag" =>
      match row_posting(row) {
        Some(p) =>
          match p.flag {
            Some(s) => QText(s)
            None => QNull
          }
        None => QNull
      }
    "other_accounts" =>
      QSet(
        transaction_accounts(row.accounts).filter(fn(a) { a != row.account }),
      )
    "number" => QNumber(row.units.number)
    "cost_number" =>
      match row.cost {
        Some(c) => QNumber(c.number)
        None => QNull
      }
    "cost_currency" =>
      match row.cost {
        Some(c) => QText(c.currency)
        None => QNull
      }
    "cost_date" =>
      match row.cost {
        Some(c) =>
          match c.date {
            Some(d) => QDate(d)
            None => QNull
          }
        None => QNull
      }
    "cost_label" =>
      QText(row.cost.map(fn(c) { c.label.unwrap_or("") }).unwrap_or(""))
    "price" =>
      match row_posting(row) {
        Some(p) => posting_price(p)
        None => QNull
      }
    "date" => QDate(row.date)
    "year" => query_int(row.date.year)
    "month" => query_int(row.date.month)
    "day" => query_int(row.date.day)
    "flag" => QText(row.flag)
    "payee" =>
      match row.payee {
        Some(s) => QText(s)
        None => QNull
      }
    "description" =>
      QText(
        match row.payee {
          Some(p) =>
            if p == "" {
              row.narration
            } else {
              p + " | " + row.narration
            }
          None => row.narration
        },
      )
    "narration" => QText(row.narration)
    "tags" => QSet(row.tags)
    "links" => QSet(row.links)
    "accounts" => QSet(transaction_accounts(row.accounts))
    "account" => QText(row.account)
    "root" => QText(@ast.account_root(row.account))
    "parent" => QText(@ast.account_parent(row.account))
    "leaf" => QText(@ast.account_leaf(row.account))
    "units" => QAmount(row.units)
    "currency" => QText(row.units.currency)
    "weight" => QAmount(row.value.unwrap_or(row.units))
    "cost" =>
      match row.cost {
        Some(cost) => QAmount(@ast.Amount::new(cost.number, cost.currency))
        None => QNull
      }
    "value" =>
      match row.value {
        Some(amount) => QAmount(amount)
        None => QNull
      }
    "position" => QPosition(row.position)
    _ => QNull
  }
}

///|
fn eval_expr(ctx : QueryContext, expr : Expr, row : BqlRow) -> QueryValue {
  match expr {
    EPredicate(cond) =>
      eval_predicate(ctx, cond, fn(e) { eval_expr(ctx, e, row) })
    EValue(value) => value
    EBinary(left, op, right) =>
      arithmetic(ctx, eval_expr(ctx, left, row), op, eval_expr(ctx, right, row))
    ECol(name) => eval_column(ctx, row, name)
    EStr(text) => QText(text)
    ENum(number) =>
      if number.scale() == 0 {
        QInt(number.mantissa())
      } else {
        QNumber(number)
      }
    EDate(date) => QDate(date)
    ENull => QNull
    EStar => QNull
    EBool(value) => QBool(value)
    ECall(name, args) =>
      match (name, args) {
        ("meta" | "entry_meta", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QText(key) => {
              if row.extra is Some(extra) && extra.cells is Some(cells) {
                let metadata = if name == "meta" {
                  cells.get("meta").unwrap_or(QNull)
                } else {
                  record_metadata(ctx, cells.get("entry").unwrap_or(QNull))
                }
                return dict_get(metadata, key).unwrap_or(QNull)
              }
              let metadata = if name == "meta" && row_posting(row) is Some(p) {
                QDict(source_metadata(p.loc, p.meta))
              } else {
                match row_entry(row) {
                  Some(e) => entry_metadata(e)
                  None =>
                    QDict(
                      (if name == "entry_meta" {
                        row.entry_meta
                      } else {
                        row.meta
                      }).entries.map(fn(p) { (p.0, meta_value(p.1)) }),
                    )
                }
              }
              dict_get(metadata, key).unwrap_or(QNull)
            }
            _ => QNull
          }
        ("getitem", [dict, key]) | ("getitem", [dict, key, _]) =>
          match (eval_expr(ctx, dict, row), eval_expr(ctx, key, row)) {
            (value, QText(k)) =>
              match dict_get(value, k) {
                Some(v) => v
                None =>
                  if args.length() == 3 {
                    eval_expr(ctx, args[2], row)
                  } else {
                    QNull
                  }
              }
            _ => QNull
          }
        ("upper" | "lower", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QText(s) => QText(unicode_case(s, name == "upper"))
            _ => QNull
          }
        ("length", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QSet(v) | QFrozenSet(v) => query_int(v.length())
            QList(v) => query_int(v.length())
            QText(s) => query_int(s.iter().count())
            _ => QNull
          }
        ("substr", [arg, start, end]) =>
          match eval_expr(ctx, arg, row) {
            QText(s) => {
              let chars = s.iter().to_array()
              let size = chars.length()
              let a = slice_index(ctx, start, row, size)
              let b = slice_index(ctx, end, row, size)
              let sb = StringBuilder()
              for i in a.. QNull
          }
        ("year", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QDate(date) => query_int(date.year)
            _ => QNull
          }
        ("month", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QDate(date) => query_int(date.month)
            _ => QNull
          }
        ("root", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QText(account) => QText(@ast.account_root(account))
            _ => QNull
          }
        ("parent", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QText(account) => QText(@ast.account_parent(account))
            _ => QNull
          }
        ("leaf", [arg]) =>
          match eval_expr(ctx, arg, row) {
            QText(account) => QText(@ast.account_leaf(account))
            _ => QNull
          }
        _ =>
          eval_function(
            ctx,
            name,
            args.map(fn(arg) { eval_expr(ctx, arg, row) }),
            row,
          )
      }
  }
}

///|
fn cond_holds(ctx : QueryContext, cond : Cond, row : BqlRow) -> Bool {
  match cond {
    CRegex(e, _, regex) =>
      match eval_expr(ctx, e, row) {
        QText(s) => regex.execute(s) is Some(_)
        _ => false
      }
    CTrue => true
    CNot(inner) => !cond_holds(ctx, inner, row)
    CAnd(left, right) =>
      cond_holds(ctx, left, row) && cond_holds(ctx, right, row)
    COr(left, right) =>
      cond_holds(ctx, left, row) || cond_holds(ctx, right, row)
    CIsNull(expr, negated) => {
      let is_null = eval_expr(ctx, expr, row) is QNull
      if negated {
        !is_null
      } else {
        is_null
      }
    }
    CTruthy(expr) =>
      match eval_expr(ctx, expr, row) {
        QSet(values) | QFrozenSet(values) => values.length() > 0
        QNull => false
        QText(text) => text != ""
        QNegativeZero(_) => false
        QNumber(number) => !number.is_zero()
        QAmount(amount) => !amount.number.is_zero()
        QPosition(position) => !position.units.number.is_zero()
        QInventory(inventory) => !inventory.is_empty()
        QBool(b) => b
        QInt(n) => n.to_string() != "0"
        QDict(v) | QRecord(_, v) => !v.is_empty()
        QList(v) => !v.is_empty()
        QEnum(_, _) | QPythonType(_) => true
        QInterval(m, d) =>
          m != @bigint.BigInt::from_int(0) || d != @bigint.BigInt::from_int(0)
        QDate(_) => true
      }
    CCmp(left, op, right) =>
      truth(
        query_comparison(
          ctx,
          eval_expr(ctx, left, row),
          op,
          eval_expr(ctx, right, row),
        ),
      )
  }
}

///|
fn expr_label(expr : Expr) -> String {
  match expr {
    EPredicate(c) => condition_label(c)
    EValue(value) => value.to_text()
    EBinary(left, op, right) => expr_label(left) + op + expr_label(right)
    ECol(name) => name
    EStr(text) => "\"" + text + "\""
    ENum(number) => number.to_string()
    EDate(date) => date.to_string()
    ENull => "null"
    EStar => "*"
    EBool(value) => if value { "true" } else { "false" }
    ECall(name, args) => {
      let sb = StringBuilder()
      sb.write_string(name)
      for i in 0.. Bool {
  match expr {
    EPredicate(c) => !aggregate_free_cond(c)
    EBinary(left, _, right) => contains_sum(left) || contains_sum(right)
    ECall("sum" | "count" | "first" | "last" | "min" | "max", _) => true
    ECall(_, args) => {
      for arg in args {
        if contains_sum(arg) {
          return true
        }
      }
      false
    }
    _ => false
  }
}

///|
fn sum_amounts(values : Array[@ast.Amount]) -> QueryValue {
  if values.length() == 0 {
    return QNull
  }
  let sums : Array[@ast.Amount] = []
  for value in values {
    let mut found = false
    for i in 0.. QueryValue {
  match expr {
    ECall("first" | "last" | "min" | "max", [arg]) => {
      let mut result = QNull
      for row in rows {
        let value = eval_expr(ctx, arg, row)
        if expr is ECall("last", _) {
          result = value
          continue
        }
        if value is QNull {
          continue
        }
        if result is QNull {
          result = value
          continue
        }
        match expr {
          ECall("min" | "max", _) =>
            if aggregate_precedes(ctx, value, result, expr is ECall("max", _)) {
              result = value
            }
          _ => ()
        }
      }
      result
    }
    ECall("count", args) => {
      let count = if args is [EStar] {
        rows.length()
      } else if args is [arg] {
        rows.filter(fn(row) { !(eval_expr(ctx, arg, row) is QNull) }).length()
      } else {
        0
      }
      query_int(count)
    }
    ECall("sum", [arg]) =>
      match arg {
        ECol("amount") => {
          let inventory = @booking.Inventory::new()
          for row in rows {
            if eval_expr(ctx, arg, row) is QAmount(amount) {
              inventory.add_amount(amount)
            }
          }
          QInventory(inventory)
        }
        ECol("position") => {
          let inventory = @booking.Inventory::new()
          for row in rows {
            inventory.add(row.position)
          }
          QInventory(inventory)
        }
        ECol("weight") => {
          let values : Array[@ast.Amount] = []
          for row in rows {
            if eval_column(ctx, row, "weight") is QAmount(a) {
              values.push(a)
            }
          }
          sum_amounts(values)
        }
        ECol("units") => {
          let values : Array[@ast.Amount] = []
          for row in rows {
            values.push(row.units)
          }
          sum_amounts(values)
        }
        ECol("cost") => {
          let values : Array[@ast.Amount] = []
          for row in rows {
            match row.cost {
              Some(cost) =>
                values.push(@ast.Amount::new(cost.number, cost.currency))
              None => ()
            }
          }
          sum_amounts(values)
        }
        ECol("value") => {
          let values : Array[@ast.Amount] = []
          for row in rows {
            match row.value {
              Some(amount) => values.push(amount)
              None => ()
            }
          }
          sum_amounts(values)
        }
        _ => {
          let kind = expression_type(arg, ctx.source, [])
          let inventory = @booking.Inventory::new()
          let mut total = @decimal.Decimal::zero()
          let mut integer_total = @bigint.BigInt::from_int(0)
          for row in rows {
            match eval_expr(ctx, arg, row) {
              QAmount(a) => inventory.add_amount(a)
              QPosition(p) => inventory.add(p)
              QInventory(i) =>
                for p in i.positions {
                  inventory.add(p)
                }
              QInt(n) => integer_total = integer_total.add(n)
              QNegativeZero(scale) =>
                total = total.add(@decimal.Decimal::zero_scaled(scale))
              QNumber(n) => total = total.add(n)
              QBool(b) =>
                if b {
                  integer_total = integer_total.add(@bigint.BigInt::from_int(1))
                }
              _ => ()
            }
          }
          if ["amount", "position", "inventory"].contains(kind) {
            QInventory(inventory)
          } else if kind == "int" || kind == "bool" {
            QInt(integer_total)
          } else {
            QNumber(total)
          }
        }
      }
    _ => QNull
  }
}

///|
fn eval_grouped(
  ctx : QueryContext,
  expr : Expr,
  rows : Array[BqlRow],
) -> QueryValue {
  if expr is EPredicate(cond) {
    eval_predicate(ctx, cond, fn(e) { eval_grouped(ctx, e, rows) })
  } else if expr is EBinary(left, op, right) {
    arithmetic(
      ctx,
      eval_grouped(ctx, left, rows),
      op,
      eval_grouped(ctx, right, rows),
    )
  } else if expr is ECall(name, args) && contains_sum(expr) {
    if ["sum", "count", "first", "last", "min", "max"].contains(name) {
      eval_aggregate(ctx, expr, rows)
    } else if !rows.is_empty() {
      eval_expr(
        ctx,
        ECall(
          name,
          args.map(fn(arg) {
            if contains_sum(arg) {
              EValue(eval_grouped(ctx, arg, rows))
            } else {
              arg
            }
          }),
        ),
        ctx.aggregate_row.unwrap_or_else(fn() { rows[0] }),
      )
    } else {
      QNull
    }
  } else if rows.length() > 0 {
    eval_expr(ctx, expr, ctx.aggregate_row.unwrap_or_else(fn() { rows[0] }))
  } else {
    QNull
  }
}

///|
priv struct OutputRow {
  values : Array[QueryValue]
  group : Array[BqlRow]
}

///|
fn group_key(ctx : QueryContext, exprs : Array[Expr], row : BqlRow) -> String {
  let sb = StringBuilder()
  for expr in exprs {
    let value = eval_expr(ctx, expr, row)
    sb.write_string(
      match value {
        QBool(b) => if b { "1" } else { "0" }
        QText(text) => "s" + text.length().to_string() + ":" + text
        QSet(_) | QFrozenSet(_) => distinct_key(value)
        QPosition(p) => position_group_key(p)
        QDict(_)
        | QRecord(_, _)
        | QList(_)
        | QInventory(_)
        | QEnum(_, _)
        | QPythonType(_) => {
          query_error(
            ctx, "query.oracle_type_error: TypeError unhashable structural value",
          )
          ""
        }
        QNegativeZero(scale) =>
          decimal_group_key(@decimal.Decimal::zero_scaled(scale))
        QNumber(n) => decimal_group_key(n)
        QAmount(a) => "amount:" + a.currency + ":" + decimal_group_key(a.number)
        _ => value.to_json()
      },
    )
    sb.write_string("\u{1f}")
  }
  sb.to_string()
}

///|
fn build_output(
  ctx : QueryContext,
  query : SelectQuery,
  rows : Array[BqlRow],
) -> Array[OutputRow] {
  let outputs : Array[OutputRow] = []
  let any_aggregate = {
    let mut found = false
    for projection in query.projections {
      if contains_sum(projection) {
        found = true
      }
    }
    found
  }
  if query.groups.length() == 0 && !any_aggregate {
    for row in rows {
      let values : Array[QueryValue] = []
      for projection in query.projections {
        values.push(eval_expr(ctx, projection, row))
      }
      outputs.push({ values, group: [row], })
    }
    return outputs
  }
  if rows.is_empty() {
    return outputs
  }
  let groups : Map[String, Array[BqlRow]] = Map([])
  let order : Array[String] = []
  if query.groups.length() == 0 {
    order.push("")
    groups.set("", rows)
  } else {
    for row in rows {
      let key = group_key(ctx, query.groups, row)
      match groups.get(key) {
        Some(bucket) => bucket.push(row)
        None => {
          groups.set(key, [row])
          order.push(key)
        }
      }
    }
  }
  for key in order {
    let bucket = match groups.get(key) {
      Some(found) => found
      None => []
    }
    let values : Array[QueryValue] = []
    for projection in query.projections {
      values.push(eval_grouped(ctx, projection, bucket))
    }
    outputs.push({ values, group: bucket, })
  }
  outputs
}

///|
fn sort_outputs(
  ctx : QueryContext,
  outputs : Array[OutputRow],
  orders : Array[OrderSpec],
  projections : Array[Expr],
) -> Unit {
  if orders.is_empty() || outputs.is_empty() {
    return
  }
  let projected = orders.map(fn(spec) {
    if spec.expr is ECol(name) {
      for i in 0..= 0 })
  let keys = outputs.map(fn(output) {
    if reuse {
      output.values
    } else {
      let values : Array[QueryValue] = []
      for i in 0..= 0 {
            output.values[projected[i]]
          } else {
            let saved = ctx.aggregate_row
            if !contains_sum(orders[i].expr) {
              ctx.aggregate_row = None
            }
            let value = eval_grouped(ctx, orders[i].expr, output.group)
            ctx.aggregate_row = saved
            value
          },
        )
      }
      values
    }
  })
  if outputs.length() < 2 || ctx.error is Some(_) {
    return
  }
  let indices : Array[Int32] = []
  for i in 0.. Int32 {
  match (a, b) {
    (QNull, QNull) => 0
    (QNull, _) => -1
    (_, QNull) => 1
    (QAmount(a), QAmount(b)) => {
      let currency = a.currency.lexical_compare(b.currency)
      if currency == 0 {
        a.number.compare(b.number)
      } else {
        currency
      }
    }
    (QPosition(a), QPosition(b)) => position_sort_compare(a, b)
    (QSet(_) | QFrozenSet(_), _) | (_, QSet(_) | QFrozenSet(_)) => {
      query_error(
        ctx, "unsupported query.unsupported.set_order: partial set ordering",
      )
      0
    }
    _ =>
      match typed_compare(ctx, a, b) {
        Some(order) => order
        None => {
          if ctx.error is None {
            query_error(
              ctx, "query.oracle_type_error: TypeError incomparable sort keys",
            )
          }
          0
        }
      }
  }
}

///|
fn run_select(
  realized : @realization.RealizedLedger,
  query : SelectQuery,
  diagnostics : Array[@ast.Diagnostic],
) -> QueryResult {
  if (query.distinct || query_any(query, representation_expr)) &&
    realized.entries.any(fn(e) { !e.query_exact_decimal }) {
    return {
      kind: Table,
      columns: [],
      rows: [],
      error: Some(
        "unsupported query.unsupported.decimal_representation: source contains signed zero; exact metadata/fingerprint representation unavailable",
      ),
    }
  }
  let ctx = QueryContext::new(realized, query.source)
  let from_filter = match query.source {
    SrcFrom(c, _, _, _, _) => c
    _ => CTrue
  }
  let origin = query_any(query, origin_expr) ||
    cond_any(from_filter, origin_expr)
  let source_realized = if query.source is SrcFrom(_, _, _, _, _) {
    {
      ..realized,
      entries: print_period(
        realized,
        print_recovered_entries(realized.entries, diagnostics),
        query.source,
      ),
    }
  } else {
    realized
  }
  let running = query_any(query, fn(e) { e is ECol("balance") })
  let all_rows = if query.source is SrcRelation(inner) {
    if inner.pivot is Some(_) {
      return {
        kind: Table,
        columns: [],
        rows: [],
        error: Some(
          "query.oracle_type_error: AttributeError pivot relation schema at BQL:1:1",
        ),
      }
    }
    let relation = run_select(realized, inner, diagnostics)
    if relation.error is Some(_) {
      return relation
    }
    relation_rows(inner.names, relation.rows)
  } else {
    source_rows(source_realized, query.source, origin)
  }
  let rows : Array[BqlRow] = []
  for row in all_rows {
    if cond_holds(ctx, from_filter, row) && cond_holds(ctx, query.filter, row) {
      if running {
        running_balance(ctx, row) |> ignore
      }
      rows.push(row)
    }
  }
  let built = build_output(ctx, query, rows)
  ctx.aggregate_row = all_rows.last()
  let mut outputs = built.filter(fn(output) {
    grouped_cond(ctx, query.having, output.group)
  })
  sort_outputs(ctx, outputs, query.orders, query.projections)
  if query.distinct {
    let unique : Array[OutputRow] = []
    let seen : Map[String, Bool] = Map([])
    for output in outputs {
      let key = output.values
        .map(distinct_key)
        .map(fn(s) { s.length().to_string() + ":" + s })
        .join("")
      if !seen.contains(key) {
        seen.set(key, true)
        unique.push(output)
      }
    }
    outputs = unique
  }
  match query.limit {
    Some(limit) =>
      if outputs.length() > limit {
        let truncated : Array[OutputRow] = []
        for i in 0.. ()
  }
  let columns = query.labels
  let result_rows : Array[Array[QueryValue]] = []
  for output in outputs {
    result_rows.push(output.values)
  }
  let result = { kind: Table, columns, rows: result_rows, error: ctx.error, }
  if query.pivot is Some((row, column)) && ctx.error is None {
    pivot_result(ctx, query, result_rows, row, column)
  } else {
    result
  }
}

///|
fn journal_row(entry : @ast.Entry, posting : @ast.Posting) -> BqlRow? {
  match posting.units {
    Some(units) => {
      let cost = posting_cost(posting, units)
      Some({
        extra: None,
        date: entry.date,
        flag: "",
        payee: None,
        narration: "",
        tags: [],
        links: [],
        meta: @ast.Meta::new(),
        entry_meta: @ast.Meta::new(),
        accounts: [],
        account: posting.account,
        units,
        cost,
        value: posting_value(units, cost, posting.price),
        position: @booking.Position::new(units, cost),
      })
    }
    None => None
  }
}

///|
fn run_journal(
  realized : @realization.RealizedLedger,
  account : String,
  limit : Int32?,
) -> QueryResult {
  let columns = ["date", "flag", "payee", "narration", "account", "position"]
  let rows : Array[Array[QueryValue]] = []
  for entry in realized.entries {
    match entry.directive {
      DTransaction(txn) =>
        for posting in booked_postings(txn.postings) {
          if posting.account == account ||
            @realization.is_subaccount(posting.account, account) {
            match journal_row(entry, posting) {
              Some(row) => {
                let done = match limit {
                  Some(max) => rows.length() >= max
                  None => false
                }
                if done {
                  break
                }
                rows.push([
                  QDate(entry.date),
                  QText(txn.flag),
                  match txn.payee {
                    Some(payee) => QText(payee)
                    None => QNull
                  },
                  QText(txn.narration),
                  QText(posting.account),
                  QPosition(row.position),
                ])
              }
              None => ()
            }
          }
        }
      _ => ()
    }
  }
  { kind: Table, columns, rows, error: None, }
}

///|
pub fn run_query(
  realized : @realization.RealizedLedger,
  text : String,
  diagnostics~ : Array[@ast.Diagnostic] = [],
) -> QueryResult {
  match parse_query(text) {
    PqError(message) =>
      { kind: Table, columns: [], rows: [], error: Some(message), }
    PqSelect(query) => run_select(realized, query, diagnostics)
    PqPrint(filter, period) => run_print(realized, filter, period, diagnostics)
    PqJournal(account, limit) => run_journal(realized, account, limit)
  }
}

///|
fn arithmetic(
  ctx : QueryContext,
  a : QueryValue,
  op : String,
  b : QueryValue,
) -> QueryValue {
  if a is QInt(x) && b is QInt(y) {
    match op {
      "+" => return QInt(x.add(y))
      "-" => return QInt(x.sub(y))
      "*" => return QInt(x.mul(y))
      "%" => {
        if y == @bigint.BigInt::from_int(0) {
          return QNull
        }
        let remainder = x % y
        return QInt(
          if remainder != @bigint.BigInt::from_int(0) &&
            (x < @bigint.BigInt::from_int(0)) !=
            (y < @bigint.BigInt::from_int(0)) {
            remainder.add(y)
          } else {
            remainder
          },
        )
      }
      _ => ()
    }
  }
  match (a, b, op) {
    (QInterval(m, d), QInt(n), "+" | "-") => {
      if n < @bigint.BigInt::from_int(-999999999) ||
        n > @bigint.BigInt::from_int(999999999) {
        query_error(
          ctx, "query.oracle_type_error: OverflowError timedelta days",
        )
        return QNull
      }
      return QInterval(m, if op == "+" { d.add(n) } else { d.sub(n) })
    }
    (QInt(n), QInterval(m, d), "+") =>
      return arithmetic(ctx, QInterval(m, d), "+", QInt(n))
    (QDate(a), QDate(b), "-") => return query_int(ordinal(a) - ordinal(b))
    (QDate(d), QInt(n), "+" | "-") =>
      return add_date_days(ctx, d, if op == "-" { n.neg() } else { n })
    (QInt(n), QDate(d), "+") => return add_date_days(ctx, d, n)
    (QDate(d), QInterval(_, _), "+" | "-") =>
      return add_interval_date(ctx, d, b, op == "-")
    (QInterval(_, _), QDate(d), "+") =>
      return add_interval_date(ctx, d, a, false)
    (QInterval(_, _), QDate(_), "-") => {
      query_error(ctx, "query.oracle_type_error: TypeError interval minus date")
      return QNull
    }
    (QInterval(am, ad), QInterval(bm, bd), "+" | "-") =>
      return if op == "+" {
        QInterval(am.add(bm), ad.add(bd))
      } else {
        QInterval(am.sub(bm), ad.sub(bd))
      }
    _ => ()
  }
  match (value_number(a), value_number(b)) {
    (Some(x), Some(y)) => {
      let result = match op {
        "+" => Some(x.add(y))
        "-" => Some(x.sub(y))
        "*" => Some(x.mul(y))
        "/" =>
          if x.is_zero() && !y.is_zero() {
            Some(@decimal.Decimal::zero_scaled(x.scale() - y.scale()))
          } else {
            x.div(y)
          }
        "%" => decimal_modulo(ctx, x, y)
        _ => None
      }
      match result {
        Some(n) =>
          if n.is_zero() && (a is QNegativeZero(_) || b is QNegativeZero(_)) {
            let negative_a = a is QNegativeZero(_) || x.is_negative()
            let negative_b = b is QNegativeZero(_) || y.is_negative()
            let negative = match op {
              "*" | "/" => negative_a != negative_b
              "+" => negative_a && negative_b
              "-" => negative_a && !negative_b
              "%" => negative_a
              _ => false
            }
            if negative {
              QNegativeZero(n.scale())
            } else {
              QNumber(n)
            }
          } else if n.is_zero() && op == "%" && x.is_negative() {
            QNegativeZero(n.scale())
          } else if n.is_zero() &&
            (op == "*" || op == "/") &&
            x.is_negative() != y.is_negative() &&
            !(a is QInt(_) && b is QInt(_) && op != "/") {
            QNegativeZero(n.scale())
          } else if a is QInt(_) && b is QInt(_) && op != "/" {
            QInt(n.mantissa())
          } else {
            QNumber(n)
          }
        None => QNull
      }
    }
    _ => QNull
  }
}

///|
fn grouped_cond(ctx : QueryContext, cond : Cond, rows : Array[BqlRow]) -> Bool {
  match cond {
    CRegex(e, _, regex) =>
      match eval_grouped(ctx, e, rows) {
        QText(s) => regex.execute(s) is Some(_)
        _ => false
      }
    CTrue => true
    CNot(c) => !grouped_cond(ctx, c, rows)
    CAnd(a, b) => grouped_cond(ctx, a, rows) && grouped_cond(ctx, b, rows)
    COr(a, b) => grouped_cond(ctx, a, rows) || grouped_cond(ctx, b, rows)
    CIsNull(e, negated) => (eval_grouped(ctx, e, rows) is QNull) != negated
    CTruthy(e) =>
      match eval_grouped(ctx, e, rows) {
        QBool(b) => b
        QNegativeZero(_) => false
        QNumber(n) => !n.is_zero()
        QNull => false
        _ => true
      }
    CCmp(a, op, b) =>
      truth(
        query_comparison(
          ctx,
          eval_grouped(ctx, a, rows),
          op,
          eval_grouped(ctx, b, rows),
        ),
      )
  }
}

///|
fn opening_rows(
  realized : @realization.RealizedLedger,
  start : @ast.Date,
  close : @ast.Date?,
) -> Array[BqlRow] {
  let inventories : Map[String, @booking.Inventory] = Map([])
  let names : Array[String] = []
  let rows : Array[BqlRow] = []
  for row in posting_rows(realized, false) {
    if row.date.less_than(start) {
      let root = @ast.account_root(row.account)
      let account = if root == "Income" || root == "Expenses" {
        realized.options.equity_account(
          "account_previous_earnings", "Earnings:Previous",
        )
      } else {
        row.account
      }
      let inventory = match inventories.get(account) {
        Some(i) => i
        None => {
          let i = @booking.Inventory::new()
          inventories.set(account, i)
          names.push(account)
          i
        }
      }
      if root == "Income" || root == "Expenses" {
        inventory.add_amount(
          match row.cost {
            Some(c) =>
              @ast.Amount::new(
                row.units.number.mul(c.number).round_to_significant(28),
                c.currency,
              )
            None => row.units
          },
        )
      } else {
        inventory.add(row.position)
      }
    } else {
      let in_period = match close {
        Some(date) => row.date.less_than(date)
        None => true
      }
      if in_period {
        rows.push(row)
      }
    }
  }
  let date = if start.day > 1 {
    @ast.Date::new(start.year, start.month, start.day - 1).unwrap()
  } else {
    let year = if start.month == 1 { start.year - 1 } else { start.year }
    let month = if start.month == 1 { 12 } else { start.month - 1 }
    @ast.Date::new(year, month, @ast.Date::days_in_month(year, month)).unwrap()
  }
  let summaries : Array[BqlRow] = []
  names.sort_by(python_text_compare)
  for account in names {
    let inventory = inventories.get(account).unwrap()
    for position in inventory.positions {
      let narration = "Opening balance for '" + account + "' (Summarization)"
      let row : BqlRow = {
        extra: None,
        date,
        flag: "S",
        payee: None,
        narration,
        tags: [],
        links: [],
        meta: @ast.Meta::new(),
        entry_meta: @ast.Meta::new(),
        accounts: [],
        account,
        units: position.units,
        cost: position.cost,
        value: posting_value(position.units, position.cost, None),
        position,
      }
      summaries.push(row)
      let amount = match position.cost {
        Some(c) =>
          @ast.Amount::new(
            position.units.number.mul(c.number).neg(),
            c.currency,
          )
        None =>
          @ast.Amount::new(position.units.number.neg(), position.units.currency)
      }
      summaries.push({
        ..row,
        account: realized.options.equity_account(
          "account_previous_balances", "Opening-Balances",
        ),
        units: amount,
        cost: None,
        value: None,
        position: @booking.Position::new(amount, None),
      })
    }
  }
  summaries + rows
}

///|
fn slice_index(
  ctx : QueryContext,
  expr : Expr,
  row : BqlRow,
  size : Int32,
) -> Int32 {
  let value = eval_expr(ctx, expr, row)
  let raw = match value {
    QBool(b) => if b { "1" } else { "0" }
    _ => value.to_text()
  }
  // Clamp before conversion, avoiding overflow for arbitrarily large indices.
  let number = @decimal.Decimal::parse(raw).unwrap()
  let bound = @decimal.Decimal::from_int(size)
  if number.compare(bound) >= 0 {
    return size
  }
  if number.compare(bound.neg()) <= 0 {
    return 0
  }
  let negative = raw.has_prefix("-")
  let mut index = 0
  for c in raw.iter() {
    if c.is_ascii_digit() {
      index = index * 10 + c.to_int() - 48
    }
  }
  if negative {
    size - index
  } else {
    index
  }
}

///|
fn transaction_rows(
  realized : @realization.RealizedLedger,
  origin : Bool,
) -> Array[BqlRow] {
  let rows : Array[BqlRow] = []
  for entry in realized.entries {
    if entry.directive is DTransaction(txn) {
      // Posting-only columns are rejected before evaluation for this source.
      let units = @ast.Amount::new(@decimal.Decimal::zero(), "")
      rows.push({
        extra: if origin {
          Some(new_row_extra(Some(entry), None, None))
        } else {
          None
        },
        date: entry.date,
        flag: txn.flag,
        payee: txn.payee,
        narration: txn.narration,
        tags: txn.tags,
        links: txn.links,
        meta: entry.meta,
        entry_meta: entry.meta,
        accounts: txn.postings,
        account: "",
        units,
        cost: None,
        value: None,
        position: @booking.Position::new(units, None),
      })
    }
  }
  rows
}

///|
fn transaction_accounts(postings : Array[@ast.Posting]) -> Array[String] {
  let accounts : Array[String] = []
  for posting in postings {
    if !accounts.contains(posting.account) {
      accounts.push(posting.account)
    }
  }
  accounts
}

///|
fn distinct_key(value : QueryValue) -> String {
  match value {
    QInt(n) => "n:" + n.to_string()
    QNegativeZero(scale) =>
      "n:" + decimal_group_key(@decimal.Decimal::zero_scaled(scale))
    QNumber(n) => "n:" + decimal_group_key(n)
    QBool(b) => if b { "n:1" } else { "n:0" }
    QSet(s) | QFrozenSet(s) => {
      let sorted = s.copy()
      sorted.sort_by(fn(a, b) { a.lexical_compare(b) })
      "set:" + sorted.map(fn(s) { s.length().to_string() + ":" + s }).join("")
    }
    QPosition(_) | QAmount(_) => "repr:" + value.to_text()
    _ => value.to_json()
  }
}

///|
fn directive_type(entry : @ast.Entry) -> String {
  match entry.directive {
    DOpen(_) => "open"
    DClose(_) => "close"
    DCommodity(_) => "commodity"
    DBalance(_) => "balance"
    DPad(_) => "pad"
    DPrice(_) => "price"
    DTransaction(_) => "transaction"
    DNote(_) => "note"
    DDocument(_) => "document"
    DEvent(_) => "event"
    DQuery(_) => "query"
    DCustom(_) => "custom"
  }
}

///|
fn directive_rows(
  realized : @realization.RealizedLedger,
  table : String,
) -> Array[BqlRow] {
  let result : Array[BqlRow] = []
  let seen : Map[String, Bool] = Map([])
  for entry in realized.entries {
    let kind = directive_type(entry)
    let selected = if table == "accounts" {
      kind == "open"
    } else {
      table == "entries" ||
      table == kind + "s" ||
      (table == "commodities" && kind == "commodity")
    }
    if !selected {
      continue
    }
    if table == "accounts" && entry.directive is DOpen(open) {
      if seen.contains(open.account) {
        continue
      }
      seen.set(open.account, true)
    }
    let units = @ast.Amount::new(@decimal.Decimal::zero(), "")
    result.push({
      extra: Some(new_row_extra(Some(entry), Some(table), None)),
      date: entry.date,
      flag: table,
      payee: None,
      narration: "",
      tags: [],
      links: [],
      meta: entry.meta,
      entry_meta: entry.meta,
      accounts: [],
      account: "",
      units,
      cost: None,
      value: None,
      position: @booking.Position::new(units, None),
    })
  }
  result
}

///|
fn directive_column(
  ctx : QueryContext,
  entry : @ast.Entry,
  name : String,
  table : String,
) -> QueryValue {
  match name {
    "discrepancy" =>
      return match entry.balance_discrepancy {
        Some(a) => QAmount(a)
        None => QNull
      }
    "id" => return QText(directive_identity(directive_value(entry)))
    "meta" => return entry_metadata(entry)
    "open" | "close" => {
      ensure_account_index(ctx)
      let account = match entry.directive {
        DOpen(o) => o.account
        _ => ""
      }
      let found = (if name == "open" { ctx.opens } else { ctx.closes })
        .unwrap()
        .get(account)
      return match found {
        Some(e) => directive_value(e)
        None => QNull
      }
    }
    "date" => return QDate(entry.date)
    "year" => return query_int(entry.date.year)
    "month" => return query_int(entry.date.month)
    "day" => return query_int(entry.date.day)
    "lineno" => return query_int(query_location(entry).line)
    "filename" =>
      match entry.directive {
        DDocument(document) =>
          if table == "documents" {
            let file = @path.Path(document.filename)
            return QText(
              if file.is_absolute() {
                file.normalize().to_string()
              } else {
                @path.Path(entry.loc.file)
                .dirname()
                .join(file)
                .normalize()
                .to_string()
              },
            )
          } else {
            return QText(query_location(entry).file)
          }
        _ => return QText(query_location(entry).file)
      }
    "type" =>
      return QText(
        match entry.directive {
          DEvent(e) => if table == "events" { e.event_type } else { "event" }
          _ => directive_type(entry)
        },
      )
    "accounts" => {
      let accounts = match entry.directive {
        DTransaction(txn) => transaction_accounts(txn.postings)
        DOpen(e) => [e.account]
        DClose(e) => [e.account]
        DBalance(e) => [e.account]
        DPad(e) => [e.account, e.source_account]
        DNote(e) => [e.account]
        DDocument(e) => [e.account]
        DCustom(_) => []
        _ => []
      }
      return QSet(accounts)
    }
    _ => ()
  }
  match entry.directive {
    DTransaction(txn) =>
      match name {
        "flag" => QText(txn.flag)
        "payee" =>
          match txn.payee {
            Some(p) => QText(p)
            None => QNull
          }
        "narration" => QText(txn.narration)
        "description" =>
          QText(
            match txn.payee {
              Some(p) =>
                if p == "" {
                  txn.narration
                } else {
                  p + " | " + txn.narration
                }
              None => txn.narration
            },
          )
        "tags" => QSet(txn.tags)
        "links" => QSet(txn.links)
        _ => QNull
      }
    DOpen(e) => if name == "account" { QText(e.account) } else { QNull }
    DClose(e) => if name == "account" { QText(e.account) } else { QNull }
    DCommodity(e) => if name == "name" { QText(e.currency) } else { QNull }
    DPrice(e) =>
      match name {
        "currency" => QText(e.currency)
        "amount" => QAmount(e.amount)
        _ => QNull
      }
    DBalance(e) =>
      match name {
        "account" => QText(e.account)
        "amount" => QAmount(e.amount)
        "tolerance" =>
          match e.tolerance {
            Some(n) => QNumber(n)
            None => QNull
          }
        _ => QNull
      }
    DEvent(e) =>
      if name == "description" && table == "events" {
        QText(e.description)
      } else {
        QNull
      }
    DNote(e) =>
      match name {
        "account" => QText(e.account)
        "comment" => QText(e.comment)
        "tags" =>
          if table == "notes" {
            QFrozenSet(e.tags)
          } else {
            QSet(e.tags)
          }
        "links" =>
          if table == "notes" {
            QFrozenSet(e.links)
          } else {
            QSet(e.links)
          }
        _ => QNull
      }
    DDocument(e) =>
      match name {
        "account" => QText(e.account)
        "tags" =>
          if table == "documents" {
            QFrozenSet(e.tags)
          } else {
            QSet(e.tags)
          }
        "links" =>
          if table == "documents" {
            QFrozenSet(e.links)
          } else {
            QSet(e.links)
          }
        _ => QNull
      }
    _ => QNull
  }
}

///|
fn decimal_group_key(number : @decimal.Decimal) -> String {
  let text = number.to_string()
  if !text.contains(".") {
    return text
  }
  let mut end = text.length()
  while end > 0 && text.get_char(end - 1) is Some('0') {
    end = end - 1
  }
  if end > 0 && text.get_char(end - 1) is Some('.') {
    end = end - 1
  }
  String::from_iter(text[:end].iter())
}