///|
fn type_argument(actual : String, expected : String) -> Bool {
actual == expected ||
(actual == "nonetype" && expected == "object") ||
(actual == "inventory" && expected == "dict") ||
expected == "any" ||
(actual == "bool" && expected == "int")
}
///|
fn function_implemented(name : String) -> Bool {
[
"upper", "lower", "length", "substr", "root", "parent", "leaf", "year", "month",
"day", "quarter", "weekday", "yearmonth", "date", "date_add", "date_diff", "meta",
"entry_meta", "any_meta", "getitem", "open_meta", "open_date", "close_date",
"commodity_meta", "currency_meta", "str", "repr", "bool", "int", "decimal", "abs",
"neg", "units", "cost", "currency", "commodity", "number", "empty", "only", "filter_currency",
"safediv", "sum", "count", "first", "last", "min", "max", "round", "splitcomp",
"account_sortkey", "getprice", "convert", "value", "possign", "date_part", "date_trunc",
"date_bin", "interval", "today", "parse_date", "joinstr", "maxwidth", "grep",
"grepn", "findfirst", "has_account", "subst",
].contains(name)
}
///|
fn expression_type(
expr : Expr,
source : QuerySource,
errors : Array[String],
) -> String {
match expr {
EPredicate(cond) => {
projection_condition_types(cond, source, errors)
"bool"
}
ECol(name) => {
let kind = column_type(source, name)
if kind == "unknown" {
errors.push("compilation query.invalid_column: " + name)
}
kind
}
EValue(value) =>
match value {
QInt(_) => "int"
QNumber(_) | QNegativeZero(_) => "decimal"
QInterval(_, _) => "relativedelta"
QList(_) => "list"
QBool(_) => "bool"
QDate(_) => "date"
QText(_) => "str"
QNull => "nonetype"
_ => "object"
}
EStr(_) => "str"
ENum(n) => if n.scale() == 0 { "int" } else { "decimal" }
EDate(_) => "date"
EBool(_) => "bool"
ENull => "nonetype"
EStar => "*"
EBinary(a, op, b) => {
let ta = expression_type(a, source, errors)
let tb = expression_type(b, source, errors)
if op == "-" && ta == "date" && tb == "date" {
"int"
} else if (op == "+" || op == "-") &&
ta == "relativedelta" &&
tb == "relativedelta" {
// The pinned oracle declares interval subtraction as date despite returning an interval.
if op == "-" {
"date"
} else {
"relativedelta"
}
} else if (op == "+" || op == "-") &&
(
(ta == "date" && ["int", "relativedelta"].contains(tb)) ||
(tb == "date" && ta == "relativedelta") ||
(tb == "date" && ta == "int" && op == "+")
) {
"date"
} else if ["int", "decimal"].contains(ta) &&
["int", "decimal"].contains(tb) {
if ta == "int" && tb == "int" && op != "/" {
"int"
} else {
"decimal"
}
} else {
errors.push(
"compilation query.invalid_signature: arithmetic " + ta + " " + tb,
)
"unknown"
}
}
ECall(name, args) => {
if name == "coalesce" {
let types = args.map(fn(e) { expression_type(e, source, errors) })
if !types.is_empty() && !types.all(fn(t) { t == types[0] }) {
errors.push(
"compilation query.invalid_signature: coalesce requires uniform types",
)
}
return types.get(0).unwrap_or("object")
}
if ["entry_meta", "any_meta"].contains(name) &&
column_type(source, "entry") == "unknown" {
errors.push("compilation query.invalid_column: entry")
}
if name == "__in" || name == "__notin" || name == "__between" {
let ts = args.map(fn(e) { expression_type(e, source, errors) })
if name == "__between" &&
!(ts.all(fn(t) { ["int", "decimal"].contains(t) }) ||
ts.all(fn(t) { t == "str" }) ||
ts.all(fn(t) { t == "date" })) {
errors.push("compilation query.invalid_signature: BETWEEN operands")
}
return "bool"
}
if source is SrcRelation(_) {
let dependency = match name {
"meta" => "meta"
"entry_meta" | "any_meta" => "entry"
"has_account" => "accounts"
_ => ""
}
if dependency != "" && column_type(source, dependency) == "unknown" {
errors.push("compilation query.invalid_column: " + dependency)
}
if name == "any_meta" && column_type(source, "meta") == "unknown" {
errors.push("compilation query.invalid_column: meta")
}
}
if name == "has_account" && column_type(source, "accounts") == "unknown" {
errors.push("compilation query.invalid_column: accounts")
}
let ts = args.map(fn(e) { expression_type(e, source, errors) })
if ts.contains("generic_set") {
errors.push("query.oracle_type_error: generic accounts type")
return "unknown"
}
let valid = oracle_signatures.any(fn(s) {
s.0 == name &&
s.1.length() == ts.length() &&
s.1.iter().zip(ts.iter()).all(fn(p) { type_argument(p.1, p.0) })
})
if !valid {
errors.push(
"compilation query.invalid_signature: " +
name +
"(" +
ts.join(",") +
")",
)
return "unknown"
}
if !function_implemented(name) {
errors.push("unsupported query.unsupported.function `" + name + "`")
return "unknown"
}
match name {
"interval" => "relativedelta"
"date_bin" | "today" | "parse_date" => "date"
"date_part" => "int"
"date_trunc" => "date"
"joinstr" | "maxwidth" | "grep" | "grepn" | "findfirst" | "subst" =>
"str"
"has_account" => "bool"
"getprice" => "decimal"
"convert" | "value" =>
if ts[0] == "inventory" {
"inventory"
} else {
"amount"
}
"possign" => ts[0]
"first" | "last" | "min" | "max" | "round" => ts[0]
"sum" =>
if ["amount", "position", "inventory"].contains(ts[0]) {
"inventory"
} else {
ts[0]
}
"count" | "length" | "year" | "month" | "day" | "date_diff" | "int" =>
"int"
"yearmonth" | "date" | "date_add" | "open_date" | "close_date" => "date"
"account_sortkey"
| "splitcomp"
| "str"
| "repr"
| "upper"
| "lower"
| "substr"
| "root"
| "parent"
| "leaf"
| "currency"
| "commodity"
| "quarter"
| "weekday" => "str"
"bool" | "empty" => "bool"
"meta" | "entry_meta" | "any_meta" | "getitem" => "object"
"open_meta" | "commodity_meta" | "currency_meta" =>
if args.length() == 1 {
"dict"
} else {
"object"
}
"decimal" | "number" | "safediv" => "decimal"
"abs" | "neg" => ts[0]
"units" | "cost" =>
if ts[0] == "position" {
"amount"
} else {
"inventory"
}
"only" => "amount"
"filter_currency" => ts[0]
_ => "unknown"
}
}
}
}
///|
fn condition_types(
cond : Cond,
source : QuerySource,
errors : Array[String],
) -> Unit {
match cond {
CTrue => ()
CNot(c) => condition_types(c, source, errors)
CAnd(a, b) | COr(a, b) => {
condition_types(a, source, errors)
condition_types(b, source, errors)
}
CCmp(a, _, b) => {
let legacy = fn(e) {
expr_any(e, fn(e) { e is ECol("units" | "cost" | "value" | "weight") })
}
if legacy(a) || legacy(b) {
expression_type(a, source, errors) |> ignore
expression_type(b, source, errors) |> ignore
} else {
projection_condition_types(cond, source, errors)
}
}
CIsNull(e, _) | CTruthy(e) | CRegex(e, _, _) =>
expression_type(e, source, errors) |> ignore
}
}