Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 134 additions & 49 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion libsql-replication/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ cbc = "0.1.2"

[dev-dependencies]
arbitrary = { version = "1.3.0", features = ["derive_arbitrary"] }
bincode = "1.3.3"
wincode = { version = "0.2", features = ["derive"] }
tempfile = "3.8.0"
prost-build = "0.12"
tonic-build = "0.11"
Expand Down
2 changes: 1 addition & 1 deletion libsql-replication/proto/proxy.proto
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ message Description {
}

message Value {
/// bincode encoded Value
/// wincode encoded Value (binary compatible with bincode)
bytes data = 1;
}

Expand Down
2 changes: 1 addition & 1 deletion libsql-replication/src/generated/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ pub struct Description {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Value {
/// / bincode encoded Value
/// / wincode encoded Value (binary compatible with bincode)
#[prost(bytes = "vec", tag = "1")]
pub data: ::prost::alloc::vec::Vec<u8>,
}
Expand Down
2 changes: 1 addition & 1 deletion libsql-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async-trait = "0.1.58"
axum = { version = "0.6.18", features = ["headers"] }
axum-extra = { version = "0.7", features = ["json-lines", "query"] }
base64 = "0.21.0"
bincode = "1.3.3"
wincode = { version = "0.2", features = ["derive"] }
bottomless = { version = "0", path = "../bottomless", features = ["libsql_linked_statically"] }
bytes = { version = "1.2.1", features = ["serde"] }
bytesize = { version = "1.2.0", features = ["serde"] }
Expand Down
10 changes: 8 additions & 2 deletions libsql-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,8 +244,14 @@ impl From<tokio::sync::oneshot::error::RecvError> for Error {
}
}

impl From<bincode::Error> for Error {
fn from(other: bincode::Error) -> Self {
impl From<wincode::WriteError> for Error {
fn from(other: wincode::WriteError) -> Self {
Self::Internal(other.to_string())
}
}

impl From<wincode::ReadError> for Error {
fn from(other: wincode::ReadError) -> Self {
Self::Internal(other.to_string())
}
}
Expand Down
3 changes: 2 additions & 1 deletion libsql-server/src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ use anyhow::{anyhow, ensure, Context};
use rusqlite::types::{ToSqlOutput, ValueRef};
use rusqlite::ToSql;
use serde::{Deserialize, Serialize};
use wincode::{SchemaRead, SchemaWrite};

use crate::query_analysis::Statement;

/// Mirrors rusqlite::Value, but implement extra traits
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, SchemaWrite, SchemaRead)]
#[cfg_attr(test, derive(arbitrary::Arbitrary))]
pub enum Value {
Null,
Expand Down
3 changes: 3 additions & 0 deletions libsql-server/src/rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ pub mod replica_proxy;
pub mod replication;
pub mod streaming_exec;

#[cfg(test)]
mod wincode_compat_test;

pub async fn run_rpc_server<A: crate::net::Accept>(
proxy_service: ProxyService,
acceptor: A,
Expand Down
10 changes: 5 additions & 5 deletions libsql-server/src/rpc/proxy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ pub mod rpc {
crate::query::Params::Named(params) => {
let iter = params.into_iter().map(|(k, v)| -> Result<_, SqldError> {
let v = Value {
data: bincode::serialize(&v)?,
data: wincode::serialize(&v)?,
};
Ok((k, v))
});
Expand All @@ -111,7 +111,7 @@ pub mod rpc {
.iter()
.map(|v| {
Ok(Value {
data: bincode::serialize(&v)?,
data: wincode::serialize(&v)?,
})
})
.collect::<Result<Vec<_>, SqldError>>()?;
Expand All @@ -130,12 +130,12 @@ pub mod rpc {
let params = pos
.values
.into_iter()
.map(|v| bincode::deserialize(&v.data).map_err(|e| e.into()))
.map(|v| wincode::deserialize(&v.data).map_err(|e| e.into()))
.collect::<Result<Vec<_>, SqldError>>()?;
Ok(Self::Positional(params))
}
query::Params::Named(named) => {
let values = named.values.iter().map(|v| bincode::deserialize(&v.data));
let values = named.values.iter().map(|v| wincode::deserialize(&v.data));
let params = itertools::process_results(values, |values| {
named.names.into_iter().zip(values).collect()
})?;
Expand Down Expand Up @@ -455,7 +455,7 @@ impl QueryResultBuilder for ExecuteResultsBuilder {
}

fn add_row_value(&mut self, v: ValueRef) -> Result<(), QueryResultBuilderError> {
let data = bincode::serialize(
let data = wincode::serialize(
&crate::query::Value::try_from(v).map_err(QueryResultBuilderError::from_any)?,
)
.map_err(QueryResultBuilderError::from_any)?;
Expand Down
101 changes: 101 additions & 0 deletions libsql-server/src/rpc/wincode_compat_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#![cfg(test)]

//! Tests to verify wincode binary compatibility with bincode 1.3.3.
//!
//! These tests ensure that the migration from bincode to wincode maintains
//! wire protocol compatibility with existing clients (particularly the Go client).

use crate::query::Value;

#[test]
fn test_wincode_produces_same_bytes_as_bincode() {
// Test that wincode produces identical bytes to what bincode 1.3.3 would produce.
// This is critical for maintaining wire protocol compatibility.

let test_cases = vec![
("Null", Value::Null, vec![0, 0, 0, 0]),
(
"Integer(42)",
Value::Integer(42),
vec![1, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0],
),
(
"Integer(i64::MAX)",
Value::Integer(i64::MAX),
vec![1, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 127],
),
(
"Real(3.14159)",
Value::Real(3.14159),
vec![2, 0, 0, 0, 110, 134, 27, 240, 249, 33, 9, 64],
),
(
"Text(empty)",
Value::Text(String::new()),
vec![3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
),
(
"Text(Hello)",
Value::Text("Hello".to_string()),
vec![3, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 72, 101, 108, 108, 111],
),
(
"Blob([1,2,3])",
Value::Blob(vec![1, 2, 3]),
vec![4, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3],
),
];

for (name, value, expected_bytes) in test_cases {
let serialised = wincode::serialize(&value).expect("wincode serialisation failed");

assert_eq!(
serialised, expected_bytes,
"{}: wincode output differs from expected bincode output",
name
);

// Also verify roundtrip
let deserialised: Value =
wincode::deserialize(&serialised).expect("wincode deserialisation failed");

assert_eq!(
format!("{:?}", value),
format!("{:?}", deserialised),
"{}: roundtrip failed",
name
);
}
}

#[test]
fn test_wincode_roundtrip_all_variants() {
// Verify that all Value variants can be serialised and deserialised correctly.
let values = vec![
Value::Null,
Value::Integer(0),
Value::Integer(i64::MIN),
Value::Integer(i64::MAX),
Value::Real(0.0),
Value::Real(std::f64::consts::PI),
Value::Real(f64::MIN),
Value::Real(f64::MAX),
Value::Text(String::new()),
Value::Text("test".to_string()),
Value::Text("unicode: 🦀🚀".to_string()),
Value::Blob(vec![]),
Value::Blob(vec![0u8, 255u8]),
Value::Blob(vec![1, 2, 3, 4, 5]),
];

for value in &values {
let encoded = wincode::serialize(value).expect("encode failed");
let decoded: Value = wincode::deserialize(&encoded).expect("decode failed");
assert_eq!(
format!("{:?}", value),
format!("{:?}", decoded),
"roundtrip failed for {:?}",
value
);
}
}
4 changes: 2 additions & 2 deletions libsql/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ bitflags = { version = "2.4.0", optional = true }
tower = { workspace = true, features = ["util"], optional = true }
worker = { version = "0.6.7", optional = true }

bincode = { version = "1", optional = true }
wincode = { version = "0.2", optional = true, features = ["derive"] }
anyhow = { version = "1.0.71", optional = true }
bytes = { version = "1.4.0", features = ["serde"], optional = true }
uuid = { version = "1.4.0", features = ["v4", "serde"], optional = true }
Expand Down Expand Up @@ -83,7 +83,7 @@ replication = [
"dep:http",
"dep:tokio",
"dep:anyhow",
"dep:bincode",
"dep:wincode",
"dep:zerocopy",
"dep:bytes",
"dep:uuid",
Expand Down
17 changes: 12 additions & 5 deletions libsql/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub enum Error {
Hrana(crate::BoxError), // Not in rusqlite
#[error("Write delegation: `{0}`")]
WriteDelegation(crate::BoxError), // Not in rusqlite
#[error("bincode: `{0}`")]
Bincode(crate::BoxError),
#[error("wincode: `{0}`")]
Wincode(crate::BoxError),
#[error("invalid column index")]
InvalidColumnIndex,
#[error("invalid column type")]
Expand Down Expand Up @@ -112,8 +112,15 @@ pub fn sqlite_errmsg_to_string(errmsg: *const std::ffi::c_char) -> String {
}

#[cfg(feature = "replication")]
impl From<bincode::Error> for Error {
fn from(e: bincode::Error) -> Self {
Error::Bincode(e.into())
impl From<wincode::WriteError> for Error {
fn from(e: wincode::WriteError) -> Self {
Error::Wincode(e.into())
}
}

#[cfg(feature = "replication")]
impl From<wincode::ReadError> for Error {
fn from(e: wincode::ReadError) -> Self {
Error::Wincode(e.into())
}
}
4 changes: 2 additions & 2 deletions libsql/src/params.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ impl From<Params> for libsql_replication::rpc::proxy::query::Params {
Params::Positional(values) => {
let values = values
.iter()
.map(|v| bincode::serialize(v).unwrap())
.map(|v| wincode::serialize(v).unwrap())
.map(|data| proxy::Value { data })
.collect::<Vec<_>>();
proxy::query::Params::Positional(proxy::Positional { values })
Expand All @@ -300,7 +300,7 @@ impl From<Params> for libsql_replication::rpc::proxy::query::Params {
let (names, values) = values
.into_iter()
.map(|(name, value)| {
let data = bincode::serialize(&value).unwrap();
let data = wincode::serialize(&value).unwrap();
let value = proxy::Value { data };
(name, value)
})
Expand Down
23 changes: 5 additions & 18 deletions libsql/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ use crate::{Error, Result};

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(
feature = "replication",
derive(wincode::SchemaWrite, wincode::SchemaRead)
)]
pub enum Value {
Null,
Integer(i64),
Expand Down Expand Up @@ -449,24 +453,7 @@ impl TryFrom<&libsql_replication::rpc::proxy::Value> for Value {
type Error = Error;

fn try_from(value: &libsql_replication::rpc::proxy::Value) -> Result<Self> {
#[derive(serde::Deserialize)]
pub enum BincodeValue {
Null,
Integer(i64),
Real(f64),
Text(String),
Blob(Vec<u8>),
}

Ok(
match bincode::deserialize::<'_, BincodeValue>(&value.data[..]).map_err(Error::from)? {
BincodeValue::Null => Value::Null,
BincodeValue::Integer(i) => Value::Integer(i),
BincodeValue::Real(x) => Value::Real(x),
BincodeValue::Text(s) => Value::Text(s),
BincodeValue::Blob(b) => Value::Blob(b),
},
)
wincode::deserialize(&value.data[..]).map_err(Error::from)
}
}

Expand Down