// Host ("Python") values accepted by the builder API, and the conversions between them and
// expressions: `maybe_parse`, `convert`, `Expr.to_py` and `Expr.__repr__` (port of the
// corresponding helpers in sqlglot/expressions/core.py).
///|
/// A host value, mirroring the Python objects that sqlglot's builder API accepts
/// (`ExpOrStr`, and the values understood by `exp.convert`).
pub(all) enum PyObj {
PyExpr(Expr)
PyStr(String)
PyInt(Int64)
PyFloat(Double)
/// A `decimal.Decimal`, kept in its textual form.
PyDecimal(String)
PyBool(Bool)
PyNone
PyTuple(Array[PyObj])
PyList(Array[PyObj])
PyDict(Array[(PyObj, PyObj)])
PyBytes(Bytes)
/// An object with named attributes (a namedtuple or an object with a `__dict__`).
PyObject(Array[(String, PyObj)])
/// A `datetime.date`.
PyDate(year~ : Int, month~ : Int, day~ : Int)
/// A `datetime.time`.
PyTime(hour~ : Int, minute~ : Int, second~ : Int, microsecond~ : Int)
/// A `datetime.datetime`; `tz` is the time zone name (`str(tzinfo)`) and its UTC offset
/// in minutes, for aware datetimes.
PyDateTime(
year~ : Int,
month~ : Int,
day~ : Int,
hour~ : Int,
minute~ : Int,
second~ : Int,
microsecond~ : Int,
tz~ : (String, Int)?
)
} derive(Eq)
///|
fn pad_int(n : Int, width : Int) -> String {
let s = n.abs().to_string()
let s = if s.length() < width {
"0".repeat(width - s.length()) + s
} else {
s
}
if n < 0 {
"-" + s
} else {
s
}
}
///|
/// Python `date.isoformat()`.
fn iso_date(year : Int, month : Int, day : Int) -> String {
"\{pad_int(year, 4)}-\{pad_int(month, 2)}-\{pad_int(day, 2)}"
}
///|
/// Python `time.isoformat()` (without a time zone).
fn iso_time(
hour : Int,
minute : Int,
second : Int,
microsecond : Int,
) -> String {
let base = "\{pad_int(hour, 2)}:\{pad_int(minute, 2)}:\{pad_int(second, 2)}"
if microsecond != 0 {
base + "." + pad_int(microsecond, 6)
} else {
base
}
}
///|
/// Python `datetime.isoformat(sep=" ")`.
fn iso_datetime(
year : Int,
month : Int,
day : Int,
hour : Int,
minute : Int,
second : Int,
microsecond : Int,
tz : (String, Int)?,
) -> String {
let base = iso_date(year, month, day) +
" " +
iso_time(hour, minute, second, microsecond)
match tz {
Some((_, offset)) => {
let sign = if offset < 0 { "-" } else { "+" }
let o = offset.abs()
base + sign + pad_int(o / 60, 2) + ":" + pad_int(o % 60, 2)
}
None => base
}
}
///|
/// Conversion of MoonBit values into host values (`PyObj`).
pub(open) trait IntoPy {
fn into_py(Self) -> PyObj
}
///|
pub impl IntoPy for PyObj with fn into_py(self) {
self
}
///|
pub impl IntoPy for Expr with fn into_py(self) {
PyExpr(self)
}
///|
pub impl IntoPy for String with fn into_py(self) {
PyStr(self)
}
///|
pub impl IntoPy for Int with fn into_py(self) {
PyInt(self.to_int64())
}
///|
pub impl IntoPy for Int64 with fn into_py(self) {
PyInt(self)
}
///|
pub impl IntoPy for Double with fn into_py(self) {
PyFloat(self)
}
///|
pub impl IntoPy for Bool with fn into_py(self) {
PyBool(self)
}
///|
pub impl IntoPy for Bytes with fn into_py(self) {
PyBytes(self)
}
///|
pub impl[T : IntoPy] IntoPy for T? with fn into_py(self) {
match self {
Some(x) => x.into_py()
None => PyNone
}
}
///|
pub impl[T : IntoPy] IntoPy for Array[T] with fn into_py(self) {
PyList(self.map(x => x.into_py()))
}
///|
pub impl[V : IntoPy] IntoPy for Map[String, V] with fn into_py(self) {
PyDict(self.iter().map(kv => (PyStr(kv.0), kv.1.into_py())).collect())
}
///|
/// Python `repr(float)`.
pub fn py_float_repr(d : Double) -> String {
if d.is_nan() {
return "nan"
}
if d.is_pos_inf() {
return "inf"
}
if d.is_neg_inf() {
return "-inf"
}
if d == 0.0 {
return if (1.0 / d).is_neg_inf() { "-0.0" } else { "0.0" }
}
// decompose MoonBit's shortest representation into digits and a decimal exponent
let s = d.abs().to_string()
let (mantissa, exp) = match s.find("e") {
Some(i) => {
let m = s.view(end_offset=i).to_string()
let e = s.view(start_offset=i + 1).to_string()
let e = if e.has_prefix("+") { e.substring(start=1) } else { e }
(m, parse_int_str(e).map(x => x.to_int()).unwrap_or(0))
}
None => (s, 0)
}
let (int_part, frac_part) = match mantissa.find(".") {
Some(i) =>
(
mantissa.view(end_offset=i).to_string(),
mantissa.view(start_offset=i + 1).to_string(),
)
None => (mantissa, "")
}
let mut digits = int_part + frac_part
// value = 0. * 10^decpt
let mut decpt = int_part.length() + exp
// strip leading zeros
let mut lead = 0
while lead < digits.length() - 1 && digits[lead] == '0' {
lead += 1
}
digits = digits.substring(start=lead)
decpt -= lead
// strip trailing zeros
let mut end = digits.length()
while end > 1 && digits[end - 1] == '0' {
end -= 1
}
digits = digits.substring(end~)
let sign = if d < 0.0 { "-" } else { "" }
let n = digits.length()
if decpt <= -4 || decpt > 16 {
let e = decpt - 1
let mant = if n == 1 {
digits
} else {
digits.substring(end=1) + "." + digits.substring(start=1)
}
let es = e.abs().to_string()
let es = if es.length() < 2 { "0" + es } else { es }
sign + mant + "e" + (if e < 0 { "-" } else { "+" }) + es
} else if decpt <= 0 {
sign + "0." + "0".repeat(-decpt) + digits
} else if decpt >= n {
sign + digits + "0".repeat(decpt - n) + ".0"
} else {
sign + digits.substring(end=decpt) + "." + digits.substring(start=decpt)
}
}
///|
/// Python `str(value)` for a host value.
pub fn PyObj::py_str(self : PyObj) -> String {
match self {
PyExpr(e) => e.to_s()
PyStr(s) => s
PyInt(i) => i.to_string()
PyFloat(d) => py_float_repr(d)
PyDecimal(s) => s
PyBool(b) => if b { "True" } else { "False" }
PyNone => "None"
PyTuple(items) =>
if items.length() == 1 {
"(\{items[0].py_repr()},)"
} else {
"(" + items.map(x => x.py_repr()).join(", ") + ")"
}
PyList(items) => "[" + items.map(x => x.py_repr()).join(", ") + "]"
PyDict(items) =>
"{" +
items.map(kv => "\{kv.0.py_repr()}: \{kv.1.py_repr()}").join(", ") +
"}"
PyBytes(b) => {
let sb = StringBuilder()
sb.write_string("b'")
for byte in b {
let c = byte.to_int()
if c == '\\'.to_int() || c == '\''.to_int() {
sb.write_char('\\')
sb.write_char(Int::unsafe_to_char(c))
} else if c >= 32 && c < 127 {
sb.write_char(Int::unsafe_to_char(c))
} else {
let hex = "0123456789abcdef"
sb.write_string("\\x")
sb.write_char(hex.get_char(c / 16).unwrap())
sb.write_char(hex.get_char(c % 16).unwrap())
}
}
sb.write_char('\'')
sb.to_string()
}
PyObject(fields) =>
"(" + fields.map(kv => "\{kv.0}=\{kv.1.py_repr()}").join(", ") + ")"
PyDate(year~, month~, day~) => iso_date(year, month, day)
PyTime(hour~, minute~, second~, microsecond~) =>
iso_time(hour, minute, second, microsecond)
PyDateTime(year~, month~, day~, hour~, minute~, second~, microsecond~, tz~) =>
iso_datetime(year, month, day, hour, minute, second, microsecond, tz)
}
}
///|
/// Python `repr(value)` for a host value.
pub fn PyObj::py_repr(self : PyObj) -> String {
match self {
PyStr(s) => py_repr_str(s)
PyDecimal(s) => "Decimal(\{py_repr_str(s)})"
PyExpr(e) => e.to_s()
_ => self.py_str()
}
}
///|
/// Python `maybe_parse`: returns the given expression (copied if `copy`), or parses the
/// given SQL code (`str(sql_or_expression)`, optionally prefixed by `prefix`) into one of
/// the kinds `into`.
pub fn[T : IntoPy] maybe_parse(
sql_or_expression : T,
into? : ArrayView[Kind],
dialect? : Dialect,
prefix? : String,
copy? : Bool = false,
error_level? : ErrorLevel = Immediate,
) -> Expr raise SqlglotError {
let obj = sql_or_expression.into_py()
match obj {
PyExpr(e) => return if copy { e.copy() } else { e }
PyNone => raise ParseError("SQL cannot be None", [])
_ => ()
}
let mut sql = obj.py_str()
match prefix {
Some(p) if !p.is_empty() => sql = "\{p} \{sql}"
_ => ()
}
parse_one(sql, dialect?, into?=into.map(i => i.to_array()), error_level~)
}
///|
/// Python `exp.convert`: converts a host value into an expression. Raises `ValueError`
/// when a conversion is not possible.
pub fn[T : IntoPy] convert(
value : T,
copy? : Bool = false,
) -> Expr raise SqlglotError {
convert_obj(value.into_py(), copy~)
}
///|
fn convert_obj(value : PyObj, copy~ : Bool) -> Expr raise SqlglotError {
match value {
PyExpr(e) => maybe_copy(e, copy)
PyStr(s) => literal_string(s)
PyBool(b) => boolean(b)
PyNone => null_()
PyFloat(d) if d.is_nan() => null_()
PyFloat(d) => literal_number(py_float_repr(d))
PyInt(i) => literal_number(i.to_string())
PyDecimal(s) => literal_number(s)
PyBytes(b) => {
let sb = StringBuilder()
let hex = "0123456789abcdef"
for byte in b {
let c = byte.to_int()
sb.write_char(hex.get_char(c / 16).unwrap())
sb.write_char(hex.get_char(c % 16).unwrap())
}
mk1(HexString, sb.to_string())
}
PyDateTime(year~, month~, day~, hour~, minute~, second~, microsecond~, tz~) => {
let datetime_literal = literal_string(
iso_datetime(year, month, day, hour, minute, second, microsecond, tz),
)
let zone = tz.map(t => literal_string(t.0))
mk(TimeStrToTime, [("this", datetime_literal), ("zone", zone)])
}
PyDate(year~, month~, day~) =>
mk1(DateStrToDate, literal_string(iso_date(year, month, day)))
PyTime(hour~, minute~, second~, microsecond~) =>
mk1(
TsOrDsToTime,
literal_string(iso_time(hour, minute, second, microsecond)),
)
PyObject(fields) => {
let exprs = []
for kv in fields {
exprs.push(
mk(PropertyEQ, [
("this", to_identifier(kv.0)),
("expression", convert_obj(kv.1, copy~)),
]),
)
}
mk(Struct, [("expressions", exprs)])
}
PyTuple(items) => {
let exprs = []
for v in items {
exprs.push(convert_obj(v, copy~))
}
mk(Tuple, [("expressions", exprs)])
}
PyList(items) => {
let exprs = []
for v in items {
exprs.push(convert_obj(v, copy~))
}
mk(Array, [("expressions", exprs)])
}
PyDict(items) => {
let keys = []
let values = []
for kv in items {
keys.push(convert_obj(kv.0, copy~))
values.push(convert_obj(kv.1, copy~))
}
mk(Map, [
("keys", mk(Array, [("expressions", keys)])),
("values", mk(Array, [("expressions", values)])),
])
}
}
}
///|
/// Python `Expr.to_py()`: returns a host value equivalent of the SQL node. Raises
/// `ValueError` for nodes that can't be converted.
pub fn Expr::to_py(self : Expr) -> PyObj raise SqlglotError {
match self.kind.owner_to_py() {
Some(Literal) => {
let this = self.text("this")
if !self.is_number() {
return PyStr(this)
}
// `int(this)` (Int64 only: larger ints raise `int64_range_error`), else
// `Decimal(this)` (kept as `str(Decimal(this))`)
match parse_int_checked(this) {
Some(i) => PyInt(i)
None =>
if is_float_str(this) {
PyDecimal(py_decimal_str(this))
} else {
raise ValueError("Invalid numeric literal: \{py_repr_str(this)}")
}
}
}
Some(Null) => PyNone
Some(Boolean) => PyBool(self.bool_arg("this"))
// `-` may be -2**63, whose absolute value is outside Int64
Some(Neg) if self.is_number() &&
self.this_().kind == Literal &&
is_int_str(self.this_().text("this")) &&
!py_strip(self.this_().text("this")).has_prefix("-") &&
!py_strip(self.this_().text("this")).has_prefix("+") =>
match parse_int_checked("-" + py_strip(self.this_().text("this"))) {
Some(i) => PyInt(i)
None => raise ValueError("Invalid numeric literal")
}
Some(Neg) if self.is_number() =>
match self.this_().to_py() {
PyInt(i) => PyInt(-i)
PyDecimal(s) =>
PyDecimal(
if s.has_prefix("-") {
s.view(start_offset=1).to_string()
} else {
"-" + s
},
)
other => other
}
_ =>
raise ValueError("\{self.to_s()} cannot be converted to a Python object.")
}
}
///|
/// Python `Expr.__repr__` (`_to_s`): a textual representation of the expression tree.
pub fn Expr::to_s(self : Expr) -> String {
to_s_node(self, 0)
}
///|
fn to_s_value(value : Value, level : Int, repr_str : Bool) -> String {
let indent = "\n" + " ".repeat(level + 1)
let delim = ",\{indent}"
match value {
Node(e) => to_s_node(e, level)
List(items) => {
let s = items.map(v => to_s_value(v, level + 1, false)).join(delim)
if s.is_empty() {
"[]"
} else {
"[\{indent}\{s}]"
}
}
Str(s) => {
let s = if repr_str { py_repr_str(s) } else { s }
// Indent multiline strings to match the current level
to_s_dedent(s).join(indent)
}
Bool(b) => if b { "True" } else { "False" }
Int(i) => i.to_string()
DT(d) => "DType.\{d}"
}
}
///|
/// `textwrap.dedent(s.strip("\n")).splitlines()`
fn to_s_dedent(s : String) -> Array[String] {
let lines = strip_chars(s, "\n").split("\n").map(l => l.to_string()).collect()
let mut margin : Int? = None
for line in lines {
if py_strip(line).is_empty() {
continue
}
let mut n = 0
for c in line {
if c == ' ' || c == '\t' {
n += 1
} else {
break
}
}
margin = match margin {
Some(m) if m <= n => Some(m)
_ => Some(n)
}
}
let m = margin.unwrap_or(0)
lines.map(line => {
if py_strip(line).is_empty() {
""
} else {
line.view(start_offset=m).to_string()
}
})
}
///|
fn to_s_node(node : Expr, level : Int) -> String {
let mut indent = "\n" + " ".repeat(level + 1)
let mut delim = ",\{indent}"
let items : Array[(String, String)] = []
let repr_str = node.is_string() ||
(node.kind == Identifier && node.bool_arg("quoted"))
for k, v in node.args {
match v {
List(l) if l.is_empty() => continue
_ => ()
}
items.push((k, to_s_value(v, level + 1, repr_str)))
}
if !node.kind.is_data_type() {
match node.get_type() {
Some(t) => items.push(("_type", to_s_node(t, level + 1)))
None => ()
}
}
match node.comments {
Some(c) if !c.is_empty() =>
items.push(
("_comments", to_s_value(List(c.map(x => Str(x))), level + 1, false)),
)
_ => ()
}
if node.is_leaf() {
indent = ""
delim = ", "
}
let body = items.map(kv => "\{kv.0}=\{kv.1}").join(delim)
"\{node.kind.name()}(\{indent}\{body})"
}
///|
/// A `DType` converts to its `DataType` expression (Python accepts a `DType` wherever a
/// `DATA_TYPE` is expected).
pub impl IntoPy for DType with fn into_py(self) {
PyExpr(mk1(DataType, self))
}