From 557c61d6db8de9f394b8a5de590fbfb3db0df412 Mon Sep 17 00:00:00 2001 From: Steven Fackler Date: Sat, 31 Aug 2013 16:11:42 -0700 Subject: [PATCH] Binary transmission of numeric types It looks like Postgres doesn't guarantee that its floats are IEEE754, but I don't know if there are any reasonable platforms on which the format is actually different. Work towards #7. --- .gitignore | 3 + Makefile | 2 +- src/test.rs | 152 +++++++++++++++++++++++++++++++++++---------------- src/types.rs | 83 +++++++++++++++++++++++----- 4 files changed, 178 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index ce9b1046..d1c16e33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ *.so +*.dylib +*.dylib.dSYM *.dummy check-* + diff --git a/Makefile b/Makefile index 9efe6864..eb15aec1 100644 --- a/Makefile +++ b/Makefile @@ -17,4 +17,4 @@ check-postgres: postgres.dummy src/test.rs .PHONY: clean clean: - rm *.dummy *.so check-* + git clean -dfx diff --git a/src/test.rs b/src/test.rs index 1af01439..a6c1da9e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,13 +1,14 @@ extern mod postgres; +use std::f32; +use std::f64; + use postgres::*; -use postgres::types::ToSql; +use postgres::types::{ToSql, FromSql}; #[test] fn test_basic() { - let conn = PostgresConnection::connect("postgres://postgres@127.0.0.1:5432"); - - do conn.in_transaction |trans| { + do test_in_transaction |trans| { trans.prepare("CREATE TABLE foo (id BIGINT PRIMARY KEY)").update([]); trans.set_rollback(); @@ -29,11 +30,18 @@ fn test_prepare_err() { } } -#[test] -fn test_query() { +fn test_in_transaction(blk: &fn(&PostgresTransaction)) { let conn = PostgresConnection::connect("postgres://postgres@127.0.0.1:5432"); do conn.in_transaction |trans| { + blk(trans); + trans.set_rollback(); + } +} + +#[test] +fn test_query() { + do test_in_transaction |trans| { trans.prepare("CREATE TABLE foo (id BIGINT PRIMARY KEY)").update([]); trans.prepare("INSERT INTO foo (id) VALUES ($1), ($2)") .update([&1 as &ToSql, &2 as &ToSql]); @@ -41,68 +49,118 @@ fn test_query() { let result = stmt.query([]); assert_eq!(~[1, 2], result.iter().map(|row| { row[0] }).collect()); - - trans.set_rollback(); - }; + } } #[test] fn test_nulls() { - let conn = PostgresConnection::connect("postgres://postgres@127.0.0.1:5432"); - - do conn.in_transaction |trans| { + do test_in_transaction |trans| { trans.prepare("CREATE TABLE foo ( - id BIGINT PRIMARY KEY, + id SERIAL PRIMARY KEY, val VARCHAR )").update([]); - trans.prepare("INSERT INTO foo (id, val) VALUES ($1, $2), ($3, $4)") - .update([&1 as &ToSql, & &"foobar" as &ToSql, - &2 as &ToSql, &None::<~str> as &ToSql]); - let stmt = trans.prepare("SELECT id, val FROM foo ORDER BY id"); + trans.prepare("INSERT INTO foo (val) VALUES ($1), ($2)") + .update([& &"foobar" as &ToSql, + &None::<~str> as &ToSql]); + let stmt = trans.prepare("SELECT val FROM foo ORDER BY id"); let result = stmt.query([]); assert_eq!(~[Some(~"foobar"), None], - result.iter().map(|row| { row[1] }).collect()); - - trans.set_rollback(); - }; -} - -#[test] -fn test_binary_bool_params() { - let conn = PostgresConnection::connect("postgres://postgres@127.0.0.1:5432"); - - do conn.in_transaction |trans| { - trans.prepare("CREATE TABLE foo ( - id BIGINT PRIMARY KEY, - b BOOL - )").update([]); - trans.prepare("INSERT INTO foo (id, b) VALUES - ($1, $2), ($3, $4), ($5, $6)") - .update([&1 as &ToSql, &true as &ToSql, - &2 as &ToSql, &false as &ToSql, - &3 as &ToSql, &None:: as &ToSql]); - let stmt = trans.prepare("SELECT b FROM foo ORDER BY id"); - let result = stmt.query([]); - - assert_eq!(~[Some(true), Some(false), None], result.iter().map(|row| { row[0] }).collect()); trans.set_rollback(); } } +fn test_param_type(sql_type: &str, values: &[T]) { + do test_in_transaction |trans| { + trans.prepare("CREATE TABLE foo ( + id SERIAL PRIMARY KEY, + b " + sql_type + + ")").update([]); + let stmt = trans.prepare("INSERT INTO foo (b) VALUES ($1)"); + for value in values.iter() { + stmt.update([value as &ToSql]); + } + + let stmt = trans.prepare("SELECT b FROM foo ORDER BY id"); + let result = stmt.query([]); + + let actual_values: ~[T] = result.iter().map(|row| { row[0] }).collect(); + assert_eq!(values, actual_values.as_slice()); + } +} + +#[test] +fn test_binary_bool_params() { + test_param_type("BOOL", [Some(true), Some(false), None]); +} + +#[test] +fn test_binary_i16_params() { + test_param_type("SMALLINT", [Some(0x0011), Some(-0x0011), None]); +} + +#[test] +fn test_binary_i32_params() { + test_param_type("INT", [Some(0x00112233), Some(-0x00112233), None]); +} + +#[test] +fn test_binary_i64_params() { + test_param_type("BIGINT", [Some(0x0011223344556677i64), + Some(-0x0011223344556677i64), None]); +} + +#[test] +fn test_binary_f32_params() { + test_param_type("REAL", [Some(f32::infinity), Some(f32::neg_infinity), + Some(1000.55), None]); +} + +#[test] +fn test_binary_f64_params() { + test_param_type("DOUBLE PRECISION", [Some(f64::infinity), + Some(f64::neg_infinity), + Some(10000.55), None]); +} + +fn test_nan_param(sql_type: &str) { + do test_in_transaction |trans| { + trans.prepare("CREATE TABLE foo ( + id SERIAL PRIMARY KEY, + b " + sql_type + + ")").update([]); + let nan: T = Float::NaN(); + trans.prepare("INSERT INTO foo (b) VALUES ($1)") + .update([&nan as &ToSql]); + + let stmt = trans.prepare("SELECT b FROM foo"); + let result = stmt.query([]); + let val: T = result.iter().next().unwrap()[0]; + assert!(val.is_NaN()); + } +} + +#[test] +fn test_f32_nan_param() { + test_nan_param::("REAL"); +} + +#[test] +fn test_f64_nan_param() { + test_nan_param::("DOUBLE PRECISION"); +} + #[test] fn test_wrong_num_params() { - let conn = PostgresConnection::connect("postgres://postgres@127.0.0.1:5432"); - - do conn.in_transaction |trans| { + do test_in_transaction |trans| { trans.prepare("CREATE TABLE foo ( - id BIGINT PRIMARY KEY, + id SERIAL PRIMARY KEY, val VARCHAR )").update([]); - let res = trans.prepare("INSERT INTO foo (id, val) VALUES ($1, $2), ($3, $4)") - .try_update([&1 as &ToSql, & &"foobar" as &ToSql]); + let res = trans.prepare("INSERT INTO foo (val) VALUES ($1), ($2)") + .try_update([& &"foobar" as &ToSql]); match res { Err(PostgresDbError { code: ~"08P01", _ }) => (), resp => fail!("Unexpected response: %?", resp) diff --git a/src/types.rs b/src/types.rs index 6466debf..e0f5579d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,9 +1,19 @@ +use std::rt::io::Decorator; +use std::rt::io::extensions::WriterByteConversions; +use std::rt::io::mem::MemWriter; use std::str; +use std::f32; +use std::f64; pub type Oid = i32; // Values from pg_type.h static BOOLOID: Oid = 16; +static INT8OID: Oid = 20; +static INT2OID: Oid = 21; +static INT4OID: Oid = 23; +static FLOAT4OID: Oid = 700; +static FLOAT8OID: Oid = 701; pub enum Format { Text = 0, @@ -79,11 +89,39 @@ from_str_impl!(u32) from_option_impl!(u32) from_str_impl!(u64) from_option_impl!(u64) -from_str_impl!(float) -from_option_impl!(float) -from_str_impl!(f32) + +impl FromSql for Option { + fn from_sql(raw: &Option<~[u8]>) -> Option { + match *raw { + None => None, + Some(ref buf) => { + Some(match str::from_bytes_slice(buf.as_slice()) { + "NaN" => f32::NaN, + "Infinity" => f32::infinity, + "-Infinity" => f32::neg_infinity, + str => FromStr::from_str(str).unwrap() + }) + } + } + } +} from_option_impl!(f32) -from_str_impl!(f64) + +impl FromSql for Option { + fn from_sql(raw: &Option<~[u8]>) -> Option { + match *raw { + None => None, + Some(ref buf) => { + Some(match str::from_bytes_slice(buf.as_slice()) { + "NaN" => f64::NaN, + "Infinity" => f64::infinity, + "-Infinity" => f64::neg_infinity, + str => FromStr::from_str(str).unwrap() + }) + } + } + } +} from_option_impl!(f64) impl FromSql for Option<~str> { @@ -122,6 +160,22 @@ macro_rules! to_option_impl( ) ) +macro_rules! to_conversions_impl( + ($oid:ident, $t:ty, $f:ident) => ( + impl ToSql for $t { + fn to_sql(&self, ty: Oid) -> (Format, Option<~[u8]>) { + if ty == $oid { + let mut writer = MemWriter::new(); + writer.$f(*self); + (Binary, Some(writer.inner())) + } else { + (Text, Some(self.to_str().into_bytes())) + } + } + } + ) +) + impl ToSql for bool { fn to_sql(&self, ty: Oid) -> (Format, Option<~[u8]>) { if ty == BOOLOID { @@ -133,16 +187,21 @@ impl ToSql for bool { } to_option_impl!(bool) +to_conversions_impl!(INT2OID, i16, write_be_i16_) +to_option_impl!(i16) +to_conversions_impl!(INT4OID, i32, write_be_i32_) +to_option_impl!(i32) +to_conversions_impl!(INT8OID, i64, write_be_i64_) +to_option_impl!(i64) +to_conversions_impl!(FLOAT4OID, f32, write_be_f32_) +to_option_impl!(f32) +to_conversions_impl!(FLOAT8OID, f64, write_be_f64_) +to_option_impl!(f64) + to_str_impl!(int) to_option_impl!(int) to_str_impl!(i8) to_option_impl!(i8) -to_str_impl!(i16) -to_option_impl!(i16) -to_str_impl!(i32) -to_option_impl!(i32) -to_str_impl!(i64) -to_option_impl!(i64) to_str_impl!(uint) to_option_impl!(uint) to_str_impl!(u8) @@ -155,10 +214,6 @@ to_str_impl!(u64) to_option_impl!(u64) to_str_impl!(float) to_option_impl!(float) -to_str_impl!(f32) -to_option_impl!(f32) -to_str_impl!(f64) -to_option_impl!(f64) impl<'self> ToSql for &'self str { fn to_sql(&self, _ty: Oid) -> (Format, Option<~[u8]>) {