///|
/// `ToValue` and `FromRaw` implementations for built-in types.
/// Core traits and types live in `value.mbt`.
///|
pub impl ToValue for Int with fn to_value(self) {
Value::Int(self)
}
///|
pub impl ToValue for Int64 with fn to_value(self) {
Value::Int64(self)
}
///|
pub impl ToValue for Double with fn to_value(self) {
Value::Float(self)
}
///|
pub impl ToValue for String with fn to_value(self) {
Value::String(self)
}
///|
pub impl ToValue for Bool with fn to_value(self) {
Value::Bool(self)
}
///|
pub impl ToValue for Bytes with fn to_value(self) {
Value::Bytes(self)
}
///|
pub impl[T : ToValue] ToValue for T? with fn to_value(self) {
match self {
None => Value::Null
Some(v) => v.to_value()
}
}
///|
/// PG `json` / `jsonb` — transmitted as text.
pub impl ToValue for Json with fn to_value(self) {
Value::String(self.stringify())
}
///|
pub impl FromRaw for Int with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Int")
Bytes(Text, b) => {
let s = decode_string(b)
@string.parse_int(s) catch {
_ => raise PgError::QueryError("cannot convert '\{s}' to Int")
}
}
Bytes(Binary, b) => decode_int4(b)
}
}
///|
pub impl FromRaw for Int64 with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Int64")
Bytes(Text, b) => {
let s = decode_string(b)
@string.parse_int64(s) catch {
_ => raise PgError::QueryError("cannot convert '\{s}' to Int64")
}
}
Bytes(Binary, b) => decode_int8(b)
}
}
///|
pub impl FromRaw for Double with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Double")
Bytes(Text, b) => {
let s = decode_string(b)
@string.parse_double(s) catch {
_ => raise PgError::QueryError("cannot convert '\{s}' to Double")
}
}
Bytes(Binary, b) => decode_float8(b)
}
}
///|
pub impl FromRaw for Bool with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Bool")
Bytes(Text, b) => {
let s = decode_string(b)
@string.parse_bool(s) catch {
_ => raise PgError::QueryError("cannot convert '\{s}' to Bool")
}
}
Bytes(Binary, b) => decode_bool(b)
}
}
///|
pub impl FromRaw for String with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("String")
Bytes(Text, b) => decode_string(b)
Bytes(Binary, b) => decode_string(b)
}
}
///|
pub impl FromRaw for Bytes with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Bytes")
Bytes(_, b) => b
}
}
///|
/// Nullable variant of `FromRaw`: `SQL NULL` decodes to `None`, any other
/// value is delegated to `T::from_raw` (which itself may raise).
pub impl[T : FromRaw] FromRaw for T? with fn from_raw(v : RawValue) raise PgError {
match v {
Null => None
Bytes(_, _) => Some(T::from_raw(v))
}
}
///|
pub impl FromRaw for Json with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Json")
Bytes(Text, b) =>
@json.parse(decode_string(b)) catch {
_ => raise PgError::QueryError("cannot parse cell as JSON")
}
Bytes(Binary, _) => raise_binary()
}
}
///|
/// PG `timestamp` / `timestamptz` — Unix epoch microseconds (UTC).
/// Transmitted as PG binary int8 (microseconds since 2000-01-01).
pub(all) struct Timestamp(Int64)
///|
/// Offset between Unix epoch (1970-01-01) and PG epoch (2000-01-01)
/// in microseconds: 30 years = 946,684,800,000,000 µs.
let pg_epoch_offset : Int64 = 946684800000000L
///|
/// Parse PG text format: `YYYY-MM-DD HH:MM:SS[.f+][+TZ]`.
/// Delegates calendar math to `@x.time.PlainDateTime`.
fn parse_ts(s : String) -> Timestamp raise PgError {
// Separate timezone offset if present (PlainDateTime ignores it).
let (body, tz_offset_secs) = split_tz(s)
// PG uses space separator; x/time expects ISO 8601 'T'.
let iso = body.replace(old=" ", new="T")
let pdt = @time.PlainDateTime::from_str(iso) catch {
_ => raise PgError::QueryError("cannot parse timestamp: '\{s}'")
}
// to_unix_second gives UTC seconds; nanosecond() gives sub-second.
// Adjust by timezone offset: e.g. +08:00 means local time is 8h ahead,
// so subtract 8h to get UTC.
let usec = (pdt.to_unix_second() - tz_offset_secs.to_int64()) * 1_000_000L +
pdt.nanosecond().to_int64() / 1000L
Timestamp(usec)
}
///|
/// Strip timezone suffix from PG timestamp string.
/// Returns `(body, offset_seconds)`. Offset is positive for `+HH:MM`
/// (ahead of UTC), negative for `-HH:MM` (behind UTC).
fn split_tz(s : String) -> (String, Int) {
// Look for + or - after the first 19 characters (date + time).
let len = s.length()
for i = 19; i < len; i = i + 1 {
if s[i] == '+' || s[i] == '-' {
let sign = if s[i] == '-' { -1 } else { 1 }
let tz_h = digit(s, i + 1) * 10 + digit(s, i + 2)
let mut tz_m = 0
let colon = i + 3
if colon < len && s[colon] == ':' {
tz_m = digit(s, colon + 1) * 10 + digit(s, colon + 2)
}
return (s[:i].to_owned(), sign * (tz_h * 3600 + tz_m * 60))
}
}
(s, 0)
}
///|
/// Single ASCII digit at position i, or 0 if out of range / non-digit.
fn digit(s : String, i : Int) -> Int {
if i >= s.length() {
return 0
}
let c = s[i].to_int() - 48
if c >= 0 && c <= 9 {
c
} else {
0
}
}
///|
pub impl ToValue for Timestamp with fn to_value(self) {
// Unix epoch → PG epoch (2000-01-01), then encode as binary int8
Value::Int64(self.0 - pg_epoch_offset)
}
///|
pub impl FromRaw for Timestamp with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Timestamp")
Bytes(Text, b) => parse_ts(decode_string(b))
Bytes(Binary, b) => Timestamp(decode_int8(b) + pg_epoch_offset)
}
}
///|
/// PG `numeric` — arbitrary-precision decimal, transmitted as text.
pub impl ToValue for @decimal.Decimal with fn to_value(self) {
Value::String(self.to_string())
}
///|
pub impl FromRaw for @decimal.Decimal with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("Decimal")
Bytes(Text, b) => {
let d = @decimal.Decimal::from_string(decode_string(b))
match d {
Some(d) => d
None => raise PgError::QueryError("cannot convert cell to Decimal")
}
}
Bytes(Binary, _) => raise_binary()
}
}
///|
/// PG `uuid` — transmitted as text.
pub impl ToValue for @uuid.UUID with fn to_value(self) {
Value::String(self.to_string())
}
///|
pub impl FromRaw for @uuid.UUID with fn from_raw(v : RawValue) raise PgError {
match v {
Null => raise_null("UUID")
Bytes(Text, b) =>
@uuid.from_hex(decode_string(b)) catch {
_ => raise PgError::QueryError("cannot convert cell to UUID")
}
Bytes(Binary, _) => raise_binary()
}
}
///|
test "parse_ts_basic" {
let ts = parse_ts("2024-07-09 12:34:56")
assert_eq(ts.0, 1720528496000000L, msg="parse_ts basic")
}
///|
test "parse_ts_with_millis" {
let ts = parse_ts("2024-07-09 12:34:56.789")
assert_eq(ts.0, 1720528496789000L, msg="parse_ts millis")
}
///|
test "parse_ts_with_micros" {
let ts = parse_ts("2024-07-09 12:34:56.789123")
assert_eq(ts.0, 1720528496789123L, msg="parse_ts micros")
}
///|
test "parse_ts_with_tz" {
// Same instant as UTC but expressed as +08:00 → should convert to UTC
let ts = parse_ts("2024-07-09 12:34:56+08")
// 12:34:56 +08:00 = 04:34:56 UTC
assert_eq(ts.0, 1720499696000000L, msg="parse_ts +08")
}
///|
test "timestamp_epoch" {
let ts = parse_ts("1970-01-01 00:00:00")
assert_eq(ts.0, 0L, msg="unix epoch")
}
///|
test "timestamp_before_epoch" {
let ts = parse_ts("1969-12-31 23:59:59")
// One second before epoch = -1_000_000 microseconds
assert_eq(ts.0, -1000000L, msg="before epoch")
}