// Values produced by BQL expressions and their text/JSON rendering.
///|
pub(all) enum QueryValue {
QText(String)
QSet(Array[String])
QNumber(@decimal.Decimal)
QDate(@ast.Date)
QAmount(@ast.Amount)
QPosition(@booking.Position)
QInventory(@booking.Inventory)
QNull
}
///|
pub(all) struct QueryResult {
columns : Array[String]
rows : Array[Array[QueryValue]]
error : String?
}
///|
pub fn QueryResult::is_ok(self : QueryResult) -> Bool {
self.error is None
}
///|
fn query_json_escape(text : String) -> String {
let sb = StringBuilder()
for i in 0.. c
None => continue
}
match ch {
'"' => sb.write_string("\\\"")
'\\' => sb.write_string("\\\\")
'\n' => sb.write_string("\\n")
'\r' => sb.write_string("\\r")
'\t' => sb.write_string("\\t")
other => sb.write_char(other)
}
}
sb.to_string()
}
///|
pub fn QueryValue::to_text(self : QueryValue) -> String {
match self {
QSet(values) => values.join(",")
QText(text) => text
QNumber(number) => number.to_string()
QDate(date) => date.to_string()
QAmount(amount) => amount.to_string()
QPosition(position) => position.to_string()
QInventory(inventory) => {
let sb = StringBuilder()
let mut first = true
for position in inventory.positions {
if !first {
sb.write_string(", ")
}
sb.write_string(position.to_string())
first = false
}
sb.to_string()
}
QNull => ""
}
}
///|
fn query_amount_json(amount : @ast.Amount) -> String {
"{\"number\":\"" +
amount.number.to_string() +
"\",\"currency\":\"" +
query_json_escape(amount.currency) +
"\"}"
}
///|
fn query_cost_json(cost : @ast.Cost?) -> String {
match cost {
Some(c) =>
"{ \"number\": \"" +
c.number.to_string() +
"\", \"currency\": \"" +
query_json_escape(c.currency) +
"\", \"date\":" +
(match c.date {
Some(d) => "\"" + d.to_string() + "\""
None => "null"
}) +
", \"label\":" +
(match c.label {
Some(l) => "\"" + query_json_escape(l) + "\""
None => "null"
}) +
" }"
None => "null"
}
}
///|
fn query_position_json(position : @booking.Position) -> String {
"{\"units\":" +
query_amount_json(position.units) +
",\"cost\":" +
query_cost_json(position.cost) +
"}"
}
///|
pub fn QueryValue::to_json(self : QueryValue) -> String {
match self {
QSet(values) =>
"[" +
values.map(fn(s) { "\"" + query_json_escape(s) + "\"" }).join(",") +
"]"
QText(text) => "\"" + query_json_escape(text) + "\""
QNumber(number) => number.to_string()
QDate(date) => "\"" + date.to_string() + "\""
QAmount(amount) => query_amount_json(amount)
QPosition(position) => query_position_json(position)
QInventory(inventory) => {
let sb = StringBuilder()
sb.write_string("[")
let mut first = true
for position in inventory.positions {
if !first {
sb.write_string(",")
}
sb.write_string(query_position_json(position))
first = false
}
sb.write_string("]")
sb.to_string()
}
QNull => "null"
}
}
///|
fn value_number(value : QueryValue) -> @decimal.Decimal? {
match value {
QNumber(number) => Some(number)
QAmount(amount) => Some(amount.number)
QPosition(position) => Some(position.units.number)
_ => None
}
}
///|
fn value_compare(a : QueryValue, b : QueryValue) -> Int32? {
match (a, b) {
(QNull, QNull) => Some(0)
(QNull, _) => None
(_, QNull) => None
(QDate(da), QDate(db)) => Some(da.compare(db))
(QText(ta), QText(tb)) => Some(ta.lexical_compare(tb))
(QAmount(aa), QAmount(ab)) =>
if aa.currency == ab.currency {
Some(aa.number.compare(ab.number))
} else {
None
}
(QPosition(pa), QPosition(pb)) =>
value_compare(QAmount(pa.units), QAmount(pb.units))
(QInventory(_), _) => None
(_, QInventory(_)) => None
_ =>
match (value_number(a), value_number(b)) {
(Some(na), Some(nb)) => Some(na.compare(nb))
_ => None
}
}
}
///|
pub fn QueryResult::to_text(self : QueryResult) -> String {
let sb = StringBuilder()
match self.error {
Some(message) => {
sb.write_string("ERROR: ")
sb.write_string(message)
sb.write_string("\n")
return sb.to_string()
}
None => ()
}
let widths : Array[Int32] = []
for column in self.columns {
widths.push(column.length())
}
let rendered : Array[Array[String]] = []
for row in self.rows {
let cells : Array[String] = []
for i in 0.. widths[i] {
widths[i] = text.length()
}
cells.push(text)
}
rendered.push(cells)
}
for i in 0.. 0 {
sb.write_string(" ")
}
sb.write_string(pad_right(self.columns[i], widths[i]))
}
sb.write_string("\n")
for i in 0.. 0 {
sb.write_string(" ")
}
let mut dashes = 0
while dashes < widths[i] {
sb.write_string("-")
dashes = dashes + 1
}
}
sb.write_string("\n")
for cells in rendered {
for i in 0.. 0 {
sb.write_string(" ")
}
sb.write_string(pad_right(cells[i], widths[i]))
}
sb.write_string("\n")
}
sb.to_string()
}
///|
fn pad_right(text : String, width : Int32) -> String {
if text.length() >= width {
return text
}
let sb = StringBuilder()
sb.write_string(text)
let mut i = text.length()
while i < width {
sb.write_string(" ")
i = i + 1
}
sb.to_string()
}
///|
pub fn QueryResult::to_json(self : QueryResult) -> String {
let sb = StringBuilder()
match self.error {
Some(message) => return "{\"error\":\"" + query_json_escape(message) + "\"}"
None => ()
}
sb.write_string("{\"columns\":[")
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string("\"")
sb.write_string(query_json_escape(self.columns[i]))
sb.write_string("\"")
}
sb.write_string("],\"rows\":[")
for r in 0.. 0 {
sb.write_string(",")
}
sb.write_string("[")
let row = self.rows[r]
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string(row[i].to_json())
}
sb.write_string("]")
}
sb.write_string("]}")
sb.to_string()
}
///|
/// Matchable query failure; the original message remains available in each case.
pub(all) enum QueryError {
Syntax(String)
Unsupported(String)
}
///|
/// Classifies parser failures without requiring hosts to match message text.
/// The legacy `error` field retains its original text for existing callers.
pub fn QueryResult::failure(self : QueryResult) -> QueryError? {
match self.error {
None => None
Some(message) =>
if message.has_prefix("unsupported ") ||
message.has_prefix("unknown function ") ||
message.has_prefix("unknown FROM source ") ||
message.has_prefix("BALANCES AT supports ") {
Some(Unsupported(message))
} else {
Some(Syntax(message))
}
}
}