Skip to content

Commit

Permalink
Merge pull request #147 from blackbeam/json-dom
Browse files Browse the repository at this point in the history
jsonb: Introduce `JsonDom` type and parsing/conversion
blackbeam authored Oct 5, 2024

Unverified

This user has not yet uploaded their public signing key.
2 parents 3d6bd8e + ec8c2ae commit a8d4d64
Showing 8 changed files with 611 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -242,6 +242,9 @@ Supported derivations:
* `#[mysql(rename = "some_name")]` – overrides column name of a field
* `#[mysql(json)]` - column will be interpreted as a JSON string containing
a value of a field type
* `#[mysql(with = path::to::convert_fn)]``convert_fn` will be used to deserialize
a field value (expects a function with a signature that mimics
`TryFrom<Value, Error=FromValueError>``)

#### Example

49 changes: 45 additions & 4 deletions src/binlog/decimal/mod.rs
Original file line number Diff line number Diff line change
@@ -49,7 +49,7 @@ pub const POWERS_10: [i32; DIG_PER_DEC + 1] = [
///
/// * serialization/deserialization to/from binary format
/// (see `read_bin` and `write_bin` functions);
/// * parsing from decimal string/buffer (see `Decimal::parse_bytes`, `FromStr` impl);
/// * parsing from decimal string/buffer (see `Decimal::parse_str_bytes`, `FromStr` impl);
/// * conversion to decimal string (using `Display`).
///
/// # Notes
@@ -58,7 +58,7 @@ pub const POWERS_10: [i32; DIG_PER_DEC + 1] = [
/// i.e. both `rhs` and `lhs` will be serialized into temporary buffers;
/// * even though MySql's `string2decimal` function allows scientific notation,
/// this implementation denies it.
#[derive(Default, Debug, Eq)]
#[derive(Default, Debug, Eq, Clone)]
pub struct Decimal {
/// The number of *decimal* digits (NOT number of `Digit`s!) before the point.
intg: usize,
@@ -75,13 +75,33 @@ impl Decimal {
decimal_bin_size(self.intg + self.frac, self.frac)
}

/// See [`Decimal::parse_str_bytes`].
#[deprecated = "use parse_str_bytes"]
pub fn parse_bytes(bytes: &[u8]) -> Result<Self, ParseDecimalError> {
match std::str::from_utf8(bytes) {
Ok(string) => Decimal::from_str(string),
Err(_) => Err(ParseDecimalError),
}
}

/// Runs `Decimal::from_str` on the given bytes.
pub fn parse_str_bytes(bytes: &[u8]) -> Result<Self, ParseDecimalError> {
macro_rules! decimal_str {
($x:ident) => {
if $x
.iter()
.all(|x| x.is_ascii_digit() || *x == b'+' || matches!(x, b'-'..=b'.'))
{
// SAFETY: UTF-8 is asserted by the if condition
Some(unsafe { std::str::from_utf8_unchecked($x) })
} else {
None
}
};
}
Decimal::from_str(decimal_str!(bytes).ok_or(ParseDecimalError)?)
}

pub fn write_bin<T: Write>(&self, mut output: T) -> io::Result<()> {
// result bits must be inverted if the sign is negative,
// we'll XOR it with `mask` to achieve this.
@@ -139,6 +159,27 @@ impl Decimal {
output.write_all(&out_buf)
}

/// Reads packed representation of a [`Decimal`].
///
/// Packed representation is:
///
/// 1. precision (u8)
/// 2. scale (u8)
/// 3. serialized decimal value (see [`Decimal::read_bin`])
pub fn read_packed<T: Read>(mut input: T, keep_precision: bool) -> io::Result<Self> {
let mut precision_and_scale = [0_u8, 0_u8];
input.read_exact(&mut precision_and_scale)?;
Self::read_bin(
input,
precision_and_scale[0] as usize,
precision_and_scale[1] as usize,
keep_precision,
)
}

/// Reads serialized representation of a decimal value.
///
/// The value is usually written in the packed form (see [`Decimal::read_packed`]).
pub fn read_bin<T: Read>(
mut input: T,
precision: usize,
@@ -158,10 +199,10 @@ impl Decimal {

// is it negative or not
let mask = if buffer.first().copied().unwrap_or(0) & 0x80 == 0 {
// positive, so mask should do noghing
// positive, so mask should do nothing
0
} else {
// negative, so mask snould invert bits
// negative, so mask should invert bits
-1
};

2 changes: 1 addition & 1 deletion src/binlog/decimal/test/mod.rs
Original file line number Diff line number Diff line change
@@ -135,7 +135,7 @@ proptest! {
let num = dbg!(&num);

// test string2decimal
let dec = dbg!(super::Decimal::parse_bytes(num.as_bytes()).unwrap());
let dec = dbg!(super::Decimal::parse_str_bytes(num.as_bytes()).unwrap());
let mysql_dec = dbg!(decimal_t::rust_string2decimal(num).unwrap());
assert_eq!(dec.intg, mysql_dec.intg as usize);
assert_eq!(dec.frac, mysql_dec.frac as usize);
281 changes: 276 additions & 5 deletions src/binlog/jsonb.rs
Original file line number Diff line number Diff line change
@@ -10,12 +10,17 @@
use std::{
borrow::Cow,
collections::BTreeMap,
convert::{TryFrom, TryInto},
fmt, io,
iter::FromIterator,
marker::PhantomData,
str::{from_utf8, Utf8Error},
};

use base64::{prelude::BASE64_STANDARD, Engine};
use serde_json::Number;

use crate::{
constants::ColumnType,
io::ParseBuf,
@@ -26,6 +31,8 @@ use crate::{
proto::{MyDeserialize, MySerialize},
};

use super::{decimal::Decimal, time::MysqlTime};

impl fmt::Debug for Value<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
@@ -119,6 +126,10 @@ impl<'a> JsonbString<'a> {
pub fn into_owned(self) -> JsonbString<'static> {
JsonbString(self.0.into_owned())
}

pub fn into_raw(self) -> Cow<'a, [u8]> {
self.0 .0
}
}

impl fmt::Debug for JsonbString<'_> {
@@ -207,7 +218,7 @@ impl<'a, T, U> ComplexValue<'a, T, U> {
}
}

/// Returns the number of lements.
/// Returns the number of elements.
pub fn element_count(&self) -> u32 {
self.element_count
}
@@ -275,8 +286,8 @@ impl<'a, T: StorageFormat> ComplexValue<'a, T, Array> {
impl<'a, T: StorageFormat, U: ComplexType> ComplexValue<'a, T, U> {
/// Returns an element at the given position.
///
/// * for arrays returns an element at the given position in an arrary,
/// * for objects returns an element with the given key index.
/// * for arrays returns an element at the given position,
/// * for objects returns an element with the given key index.
///
/// Returns `None` if `pos >= self.element_count()`.
pub fn elem_at(&'a self, pos: u32) -> io::Result<Option<Value<'a>>> {
@@ -416,9 +427,140 @@ impl<'a> OpaqueValue<'a> {
data: self.data.into_owned(),
}
}

pub fn into_data(self) -> Cow<'a, [u8]> {
self.data.0
}
}

/// Jsonb Value.
/// Structured in-memory representation of a JSON value.
///
/// You can get this value using [`Value::parse`].
///
/// You can convert this into a [`serde_json::Value`] (using [`From`] impl). Opaque values will
/// be handled as follows:
///
/// * [`ColumnType::MYSQL_TYPE_NEWDECIMAL`] — will be converted to string
/// * [`ColumnType::MYSQL_TYPE_DATE`] — will be converted to 'YYYY-MM-DD' string
/// * [`ColumnType::MYSQL_TYPE_TIME`] — will be converted to '[-][h]hh:mm::ss.µµµµµµ' string
/// * [`ColumnType::MYSQL_TYPE_DATETIME`] and [`ColumnType::MYSQL_TYPE_TIMESTAMP`]
/// — will be converted to 'YYYY-MM-DD hh:mm::ss.µµµµµµ' string
/// * other opaque values will be represented as strings in the form `base64:type<id>:<data>`
/// where:
/// - `<id>` — [`ColumnType`] integer value
/// - `<data>` — base64-encoded opaque data
#[derive(Debug, Clone, PartialEq)]
pub enum JsonDom {
Container(JsonContainer),
Scalar(JsonScalar),
}

impl From<JsonDom> for serde_json::Value {
fn from(value: JsonDom) -> Self {
match value {
JsonDom::Container(json_container) => json_container.into(),
JsonDom::Scalar(json_scalar) => json_scalar.into(),
}
}
}

/// [`JsonDom`] container.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonContainer {
Array(Vec<JsonDom>),
Object(BTreeMap<String, JsonDom>),
}

impl From<JsonContainer> for serde_json::Value {
fn from(value: JsonContainer) -> Self {
match value {
JsonContainer::Array(vec) => {
serde_json::Value::Array(Vec::from_iter(vec.into_iter().map(|x| x.into())))
}
JsonContainer::Object(btree_map) => serde_json::Value::Object(
serde_json::Map::from_iter(btree_map.into_iter().map(|(k, v)| (k, v.into()))),
),
}
}
}

/// [`JsonDom`] scalar value.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonScalar {
Boolean(bool),
DateTime(MysqlTime),
Null,
Number(JsonNumber),
Opaque(JsonOpaque),
String(String),
}

impl From<JsonScalar> for serde_json::Value {
fn from(value: JsonScalar) -> Self {
match value {
JsonScalar::Boolean(x) => serde_json::Value::Bool(x),
JsonScalar::DateTime(mysql_time) => {
serde_json::Value::String(format!("{:.6}", mysql_time))
}
JsonScalar::Null => serde_json::Value::Null,
JsonScalar::Number(json_number) => json_number.into(),
JsonScalar::Opaque(json_opaque) => json_opaque.into(),
JsonScalar::String(x) => serde_json::Value::String(x),
}
}
}

/// [`JsonDom`] number.
#[derive(Debug, Clone, PartialEq)]
pub enum JsonNumber {
Decimal(Decimal),
Double(f64),
Int(i64),
Uint(u64),
}

impl From<JsonNumber> for serde_json::Value {
fn from(value: JsonNumber) -> Self {
match value {
JsonNumber::Decimal(decimal) => serde_json::Value::String(decimal.to_string()),
JsonNumber::Double(x) => serde_json::Value::Number(
Number::from_f64(x)
// infinities an NaN are rendered as `0`
.unwrap_or_else(|| Number::from(0_u64)),
),
JsonNumber::Int(x) => serde_json::Value::Number(Number::from(x)),
JsonNumber::Uint(x) => serde_json::Value::Number(Number::from(x)),
}
}
}

/// [`JsonDom`] opaque value.
#[derive(Debug, Clone, PartialEq)]
pub struct JsonOpaque {
field_type: ColumnType,
value: Vec<u8>,
}

impl fmt::Display for JsonOpaque {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"base64:type{}:{}",
self.field_type as u8,
BASE64_STANDARD.encode(&self.value)
)
}
}

impl From<JsonOpaque> for serde_json::Value {
fn from(value: JsonOpaque) -> Self {
serde_json::Value::String(value.to_string())
}
}

/// Deserialized Jsonb value.
///
/// You can [`Value::parse`] it to a structured [`JsonDom`] value.
#[derive(Clone, PartialEq)]
pub enum Value<'a> {
Null,
@@ -537,7 +679,9 @@ impl<'a> Value<'a> {
matches!(self, Value::F64(_))
}

/// Returns the number of lements in array or buffer.
/// Returns the number of elements in array or object.
///
/// Returns `None` on none-array/non-object values.
pub fn element_count(&self) -> Option<u32> {
match self {
Value::SmallArray(x) => Some(x.element_count()),
@@ -549,12 +693,139 @@ impl<'a> Value<'a> {
}

/// Returns the field type of an opaque value.
///
/// Returns `None` on non-opaque values.
pub fn field_type(&self) -> Option<ColumnType> {
match self {
Value::Opaque(OpaqueValue { value_type, .. }) => Some(**value_type),
_ => None,
}
}

/// Parse this value to a structured representation.
pub fn parse(self) -> io::Result<JsonDom> {
match self {
Value::Null => Ok(JsonDom::Scalar(JsonScalar::Null)),
Value::Bool(value) => Ok(JsonDom::Scalar(JsonScalar::Boolean(value))),
Value::I16(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Int(
x as i64,
)))),
Value::U16(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Uint(
x as u64,
)))),
Value::I32(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Int(
x as i64,
)))),
Value::U32(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Uint(
x as u64,
)))),
Value::I64(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Int(x)))),
Value::U64(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Uint(x)))),
Value::F64(x) => Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Double(x)))),
Value::String(jsonb_string) => {
let s = match jsonb_string.into_raw() {
Cow::Borrowed(x) => Cow::Borrowed(
std::str::from_utf8(x)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
),
Cow::Owned(x) => Cow::Owned(
String::from_utf8(x)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
),
};
Ok(JsonDom::Scalar(JsonScalar::String(s.into_owned())))
}
Value::SmallArray(complex_value) => {
let mut elements = Vec::with_capacity(complex_value.element_count() as usize);
for i in 0.. {
if let Some(value) = complex_value.elem_at(i)? {
let y = value.parse()?;
elements.push(y);
} else {
break;
}
}
Ok(JsonDom::Container(JsonContainer::Array(elements)))
}
Value::LargeArray(complex_value) => {
let mut elements = Vec::with_capacity(complex_value.element_count() as usize);
for value in complex_value.iter() {
elements.push(value?.parse()?);
}
Ok(JsonDom::Container(JsonContainer::Array(elements)))
}
Value::SmallObject(complex_value) => {
let mut elements = BTreeMap::new();
for value in complex_value.iter() {
let (key, value) = value?;
elements.insert(key.value().into_owned(), value.parse()?);
}
Ok(JsonDom::Container(JsonContainer::Object(elements)))
}
Value::LargeObject(complex_value) => {
let mut elements = BTreeMap::new();
for value in complex_value.iter() {
let (key, value) = value?;
elements.insert(key.value().into_owned(), value.parse()?);
}
Ok(JsonDom::Container(JsonContainer::Object(elements)))
}
Value::Opaque(opaque_value) => match opaque_value.value_type() {
ColumnType::MYSQL_TYPE_NEWDECIMAL => {
let data = opaque_value.data_raw();

Ok(JsonDom::Scalar(JsonScalar::Number(JsonNumber::Decimal(
Decimal::read_packed(data, false)?,
))))
}
ColumnType::MYSQL_TYPE_DATE => {
let packed_value =
opaque_value.data_raw().first_chunk::<8>().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"not enough data to decode MYSQL_TYPE_DATE",
)
})?;
let packed_value = i64::from_le_bytes(*packed_value);
Ok(JsonDom::Scalar(JsonScalar::DateTime(
MysqlTime::from_int64_date_packed(packed_value),
)))
}
ColumnType::MYSQL_TYPE_TIME => {
let packed_value = dbg!(opaque_value.data_raw())
.first_chunk::<8>()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"not enough data to decode MYSQL_TYPE_TIME",
)
})?;
let packed_value = dbg!(i64::from_le_bytes(*packed_value));
Ok(JsonDom::Scalar(JsonScalar::DateTime(
MysqlTime::from_int64_time_packed(packed_value),
)))
}
ColumnType::MYSQL_TYPE_DATETIME | ColumnType::MYSQL_TYPE_TIMESTAMP => {
let packed_value =
opaque_value.data_raw().first_chunk::<8>().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
"not enough data to decode MYSQL_TYPE_DATETIME",
)
})?;
let packed_value = i64::from_le_bytes(*packed_value);
Ok(JsonDom::Scalar(JsonScalar::DateTime(
MysqlTime::from_int64_datetime_packed(packed_value),
)))
}

field_type => Ok(JsonDom::Scalar(JsonScalar::Opaque(JsonOpaque {
field_type,
value: opaque_value.data.0.into_owned(),
}))),
},
}
}
}

impl<'a> TryFrom<Value<'a>> for serde_json::Value {
77 changes: 77 additions & 0 deletions src/binlog/mod.rs
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ pub mod jsonb;
pub mod jsondiff;
pub mod misc;
pub mod row;
pub mod time;
pub mod value;

pub struct BinlogCtx<'a> {
@@ -854,6 +855,82 @@ mod tests {
}
}

if file_path.file_name().unwrap() == "json-opaque.binlog" {
let event_data = ev.read_data().unwrap();

/// Extracts first column of the binlog row after-image as a Jsonb::Value
/// then parses it into the structured representation and compares with
/// the expected value.
macro_rules! extract_cmp {
($row:expr, $expected:tt) => {
let mut after = $row.1.unwrap().unwrap();
let a = dbg!(after.pop().unwrap());
let super::value::BinlogValue::Jsonb(a) = a else {
panic!("BinlogValue::Jsonb(_) expected");
};
assert_eq!(
serde_json::json!($expected),
serde_json::Value::from(a.parse().unwrap())
);
};
}

match event_data {
Some(EventData::RowsEvent(ev)) if i == 10 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"a": "base64:type15:VQ=="});
}
Some(EventData::RowsEvent(ev)) if i == 12 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"b": "2012-03-18"});
}
Some(EventData::RowsEvent(ev)) if i == 14 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"c": "2012-03-18 11:30:45.000000"});
}
Some(EventData::RowsEvent(ev)) if i == 16 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"c": "87:31:46.654321"});
}
Some(EventData::RowsEvent(ev)) if i == 18 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"d": "123.456"});
}
Some(EventData::RowsEvent(ev)) if i == 20 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"e": "9.00"});
}
Some(EventData::RowsEvent(ev)) if i == 22 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"e": [0, 1, true, false]});
}
Some(EventData::RowsEvent(ev)) if i == 24 => {
let table_map_event =
binlog_file.reader().get_tme(ev.table_id()).unwrap();
let mut rows = ev.rows(table_map_event);
extract_cmp!(rows.next().unwrap().unwrap(), {"e": null});
}
Some(EventData::RowsEvent(ev)) => {
panic!("no more events expected i={}, {:?}", i, ev);
}
_ => (),
}
}

if file_path.file_name().unwrap() == "vector.binlog" {
let event_data = ev.read_data().unwrap();
match event_data {
208 changes: 208 additions & 0 deletions src/binlog/time.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use std::{
cmp::min,
fmt::{self, Write},
};

use super::misc::{my_packed_time_get_frac_part, my_packed_time_get_int_part};

/// Server-side mysql time representation.
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct MysqlTime {
pub year: u32,
pub month: u32,
pub day: u32,
pub hour: u32,
pub minute: u32,
pub second: u32,
/// microseconds
pub second_part: u32,
pub neg: bool,
pub time_type: MysqlTimestampType,
pub time_zone_displacement: i32,
}

impl MysqlTime {
/// Convert time packed numeric representation to [`MysqlTime`].
pub fn from_int64_time_packed(mut packed_value: i64) -> Self {
let neg = packed_value < 0;
if neg {
packed_value = -packed_value
}

let hms: i64 = my_packed_time_get_int_part(packed_value);

let hour = (hms >> 12) as u32 % (1 << 10); /* 10 bits starting at 12th */
let minute = (hms >> 6) as u32 % (1 << 6); /* 6 bits starting at 6th */
let second = hms as u32 % (1 << 6); /* 6 bits starting at 0th */
let second_part = my_packed_time_get_frac_part(packed_value);

Self {
year: 0,
month: 0,
day: 0,
hour,
minute,
second,
second_part: second_part as u32,
neg,
time_type: MysqlTimestampType::MYSQL_TIMESTAMP_TIME,
time_zone_displacement: 0,
}
}

/// Convert packed numeric date representation to [`MysqlTime`].
pub fn from_int64_date_packed(packed_value: i64) -> Self {
let mut this = Self::from_int64_datetime_packed(packed_value);
this.time_type = MysqlTimestampType::MYSQL_TIMESTAMP_DATE;
this
}

/// Convert packed numeric datetime representation to [`MysqlTime`].
pub fn from_int64_datetime_packed(mut packed_value: i64) -> Self {
let neg = packed_value < 0;
if neg {
packed_value = -packed_value
}

let second_part = my_packed_time_get_frac_part(packed_value);
let ymdhms: i64 = my_packed_time_get_int_part(packed_value);

let ymd: i64 = ymdhms >> 17;
let ym: i64 = ymd >> 5;
let hms: i64 = ymdhms % (1 << 17);

let day = ymd % (1 << 5);
let month = ym % 13;
let year = (ym / 13) as _;

let second = hms % (1 << 6);
let minute = (hms >> 6) % (1 << 6);
let hour = (hms >> 12) as _;

Self {
year,
month: month as _,
day: day as _,
hour,
minute: minute as _,
second: second as _,
second_part: second_part as _,
neg,
time_type: MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME,
time_zone_displacement: 0,
}
}
}

impl fmt::Display for MysqlTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.time_type {
MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME
| MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME_TZ => format_datetime(self, f),
MysqlTimestampType::MYSQL_TIMESTAMP_DATE => format_date(self, f),
MysqlTimestampType::MYSQL_TIMESTAMP_TIME => format_time(self, f),
MysqlTimestampType::MYSQL_TIMESTAMP_NONE
| MysqlTimestampType::MYSQL_TIMESTAMP_ERROR => Ok(()),
}
}
}

fn trim_two_digits(value: u32) -> u32 {
if value >= 100 {
0
} else {
value
}
}

/// Formats a time value as `HH:MM:SS[.fraction]`.
fn format_time(time: &MysqlTime, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if time.neg {
f.write_char('-')?;
}

write!(
f,
"{:02}:{:02}:{:02}",
time.hour,
trim_two_digits(time.minute),
trim_two_digits(time.second),
)?;
format_useconds(time.second_part, f)?;
Ok(())
}

/// Formats a datetime value with an optional fractional part (if formatter precision is given).
fn format_datetime(time: &MysqlTime, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_date_and_time(time, f)?;
format_useconds(time.second_part, f)?;
if time.time_type == MysqlTimestampType::MYSQL_TIMESTAMP_DATETIME_TZ {
format_tz(time.time_zone_displacement, f)?;
}
Ok(())
}

/// Formats date and time part as 'YYYY-MM-DD hh:mm:ss'
fn format_date_and_time(time: &MysqlTime, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:02}{:02}-{:02}-{:02} {:02}:{:02}:{:02}",
trim_two_digits(time.year / 100),
trim_two_digits(time.year % 100),
trim_two_digits(time.month),
trim_two_digits(time.day),
trim_two_digits(time.hour),
trim_two_digits(time.minute),
trim_two_digits(time.second),
)
}

/// Formats a date value as 'YYYY-MM-DD'.
fn format_date(time: &MysqlTime, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:02}{:02}-{:02}-{:02}",
trim_two_digits(time.year / 100),
trim_two_digits(time.year % 100),
trim_two_digits(time.month),
trim_two_digits(time.day),
)
}

/// Only formats useconds if formatter precision is given (will be truncated to 6)
fn format_useconds(mut useconds: u32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Some(dec) = f.precision().map(|x| min(x, 6)) else {
return Ok(());
};

if dec == 0 {
return Ok(());
}

useconds %= 1_000_000;

for _ in 0..(6 - dec) {
useconds /= 10;
}

write!(f, ".{:0width$}", useconds, width = dec)
}

fn format_tz(tzd: i32, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "+{:02}:{:02}", tzd / 3600, tzd.abs() / 60 % 60)
}

#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
#[allow(non_camel_case_types)]
pub enum MysqlTimestampType {
/// Textual representation of this value is an empty string
MYSQL_TIMESTAMP_NONE = -2,
/// Textual representation of this value is an empty string
MYSQL_TIMESTAMP_ERROR = -1,
MYSQL_TIMESTAMP_DATE = 0,
MYSQL_TIMESTAMP_DATETIME = 1,
MYSQL_TIMESTAMP_TIME = 2,
MYSQL_TIMESTAMP_DATETIME_TZ = 3,
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
@@ -333,7 +333,7 @@

// The `test` feature is required to compile tests.
// It'll bind test binaries to an official C++ impl of MySql decimals (see build.rs)
// The idea is to test our rust impl agaist C++ impl.
// The idea is to test our rust impl against C++ impl.
#[cfg(all(not(feature = "test"), test))]
compile_error!("Please invoke `cargo test` with `--features test` flags");

Binary file added test-data/binlogs/json-opaque.binlog
Binary file not shown.

0 comments on commit a8d4d64

Please sign in to comment.