diff --git a/src/sql_jsc/mysql/MySQLValue.rs b/src/sql_jsc/mysql/MySQLValue.rs index 0b3af63a8d09..ce29da3fae78 100644 --- a/src/sql_jsc/mysql/MySQLValue.rs +++ b/src/sql_jsc/mysql/MySQLValue.rs @@ -182,6 +182,38 @@ impl Drop for Bytes { // No explicit Drop for Value: the enum payloads (ZigStringSlice, Bytes, Data) all impl Drop. +/// The integer branches of `Value::from_js` validate against the full range of +/// the target type, so the bounds are derived from `T` rather than repeated at +/// every call site. +fn int_range(field_name: &'static [u8]) -> IntegerRange { + IntegerRange { + min: T::MIN_I128, + max: T::MAX_I128, + field_name, + ..Default::default() + } +} + +fn validate_int( + global_object: &JSGlobalObject, + value: JSValue, + field_name: &'static [u8], +) -> Result { + global_object + .validate_integer_range::(value, T::ZERO, int_range::(field_name)) + .map_err(js_error_to_mysql) +} + +fn validate_bigint( + global_object: &JSGlobalObject, + value: JSValue, + field_name: &'static [u8], +) -> Result { + global_object + .validate_big_int_range::(value, T::ZERO, int_range::(field_name)) + .map_err(js_error_to_mysql) +} + impl Value { pub fn to_data(&self, field_type: FieldType) -> Result { let mut buffer = [0u8; 15]; // Large enough for all fixed-size types @@ -277,99 +309,24 @@ impl Value { FieldType::MYSQL_TYPE_TINY => Ok(Value::Bool(value.to_boolean())), FieldType::MYSQL_TYPE_SHORT => { if unsigned { - return Ok(Value::Ushort( - global_object - .validate_integer_range::( - value, - 0, - IntegerRange { - min: u16::MIN as i128, - max: u16::MAX as i128, - field_name: b"u16", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )); + Ok(Value::Ushort(validate_int(global_object, value, b"u16")?)) + } else { + Ok(Value::Short(validate_int(global_object, value, b"i16")?)) } - Ok(Value::Short( - global_object - .validate_integer_range::( - value, - 0, - IntegerRange { - min: i16::MIN as i128, - max: i16::MAX as i128, - field_name: b"i16", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )) } FieldType::MYSQL_TYPE_LONG => { if unsigned { - return Ok(Value::Uint( - global_object - .validate_integer_range::( - value, - 0, - IntegerRange { - min: u32::MIN as i128, - max: u32::MAX as i128, - field_name: b"u32", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )); + Ok(Value::Uint(validate_int(global_object, value, b"u32")?)) + } else { + Ok(Value::Int(validate_int(global_object, value, b"i32")?)) } - Ok(Value::Int( - global_object - .validate_integer_range::( - value, - 0, - IntegerRange { - min: i32::MIN as i128, - max: i32::MAX as i128, - field_name: b"i32", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )) } FieldType::MYSQL_TYPE_LONGLONG => { if unsigned { - return Ok(Value::Ulong( - global_object - .validate_big_int_range::( - value, - 0, - IntegerRange { - min: 0, - max: u64::MAX as i128, - field_name: b"u64", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )); + Ok(Value::Ulong(validate_bigint(global_object, value, b"u64")?)) + } else { + Ok(Value::Long(validate_bigint(global_object, value, b"i64")?)) } - Ok(Value::Long( - global_object - .validate_big_int_range::( - value, - 0, - IntegerRange { - min: i64::MIN as i128, - max: i64::MAX as i128, - field_name: b"i64", - ..Default::default() - }, - ) - .map_err(js_error_to_mysql)?, - )) } FieldType::MYSQL_TYPE_FLOAT => Ok(Value::Float( diff --git a/src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs b/src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs index 3d1a04e466c8..f46238ec4fe6 100644 --- a/src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs +++ b/src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs @@ -1,7 +1,6 @@ use crate::jsc::JSGlobalObject; use crate::mysql::my_sql_value::{DateTime, Time}; use crate::shared::sql_data_cell::SQLDataCell; -use crate::shared::sql_data_cell::{Tag as CellTag, Value as CellValue}; use bun_sql::mysql::mysql_types as types; use bun_sql::mysql::mysql_types::FieldType; use bun_sql::mysql::protocol::new_reader::{NewReader, ReaderContext}; @@ -37,18 +36,10 @@ pub fn decode_binary_value( } let val = reader.byte()?; if unsigned { - return Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { uint4: val as u32 }, - ..Default::default() - }); + return Ok(SQLDataCell::uint4(val as u32)); } let ival: i8 = val as i8; - Ok(SQLDataCell { - tag: CellTag::Int4, - value: CellValue { int4: ival as i32 }, - ..Default::default() - }) + Ok(SQLDataCell::int4(ival as i32)) } FieldType::MYSQL_TYPE_SHORT => { if raw { @@ -56,21 +47,9 @@ pub fn decode_binary_value( return Ok(SQLDataCell::raw(Some(&data))); } if unsigned { - return Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { - uint4: reader.int::()? as u32, - }, - ..Default::default() - }); + return Ok(SQLDataCell::uint4(reader.int::()? as u32)); } - Ok(SQLDataCell { - tag: CellTag::Int4, - value: CellValue { - int4: reader.int::()? as i32, - }, - ..Default::default() - }) + Ok(SQLDataCell::int4(reader.int::()? as i32)) } FieldType::MYSQL_TYPE_YEAR => { // Binary protocol sends YEAR as a fixed 2-byte unsigned field; @@ -79,13 +58,7 @@ pub fn decode_binary_value( let data = reader.read(2)?; return Ok(SQLDataCell::raw(Some(&data))); } - Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { - uint4: reader.int::()? as u32, - }, - ..Default::default() - }) + Ok(SQLDataCell::uint4(reader.int::()? as u32)) } FieldType::MYSQL_TYPE_INT24 => { if raw { @@ -95,21 +68,9 @@ pub fn decode_binary_value( return Ok(SQLDataCell::raw(Some(&data.substring(0, 3)))); } if unsigned { - return Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { - uint4: reader.int_u24()?, - }, - ..Default::default() - }); + return Ok(SQLDataCell::uint4(reader.int_u24()?)); } - Ok(SQLDataCell { - tag: CellTag::Int4, - value: CellValue { - int4: reader.int_i24()?, - }, - ..Default::default() - }) + Ok(SQLDataCell::int4(reader.int_i24()?)) } FieldType::MYSQL_TYPE_LONG => { if raw { @@ -117,21 +78,9 @@ pub fn decode_binary_value( return Ok(SQLDataCell::raw(Some(&data))); } if unsigned { - return Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { - uint4: reader.int::()?, - }, - ..Default::default() - }); + return Ok(SQLDataCell::uint4(reader.int::()?)); } - Ok(SQLDataCell { - tag: CellTag::Int4, - value: CellValue { - int4: reader.int::()?, - }, - ..Default::default() - }) + Ok(SQLDataCell::int4(reader.int::()?)) } FieldType::MYSQL_TYPE_LONGLONG => { if raw { @@ -140,107 +89,45 @@ pub fn decode_binary_value( if unsigned { let val = reader.int::()?; if val <= u32::MAX as u64 { - return Ok(SQLDataCell { - tag: CellTag::Uint4, - value: CellValue { - uint4: u32::try_from(val).expect("int cast"), - }, - ..Default::default() - }); + return Ok(SQLDataCell::uint4(u32::try_from(val).expect("int cast"))); } if bigint { - return Ok(SQLDataCell { - tag: CellTag::Uint8, - value: CellValue { uint8: val }, - ..Default::default() - }); + return Ok(SQLDataCell::uint8(val)); } let mut buffer = bun_core::fmt::ItoaBuf::new(); let slice = bun_core::fmt::itoa(&mut buffer, val); - return Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }); + return Ok(SQLDataCell::string(slice)); } let val = reader.int::()?; if val >= i32::MIN as i64 && val <= i32::MAX as i64 { - return Ok(SQLDataCell { - tag: CellTag::Int4, - value: CellValue { - int4: i32::try_from(val).expect("int cast"), - }, - ..Default::default() - }); + return Ok(SQLDataCell::int4(i32::try_from(val).expect("int cast"))); } if bigint { - return Ok(SQLDataCell { - tag: CellTag::Int8, - value: CellValue { int8: val }, - ..Default::default() - }); + return Ok(SQLDataCell::int8(val)); } let mut buffer = bun_core::fmt::ItoaBuf::new(); let slice = bun_core::fmt::itoa(&mut buffer, val); - Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(slice)) } FieldType::MYSQL_TYPE_FLOAT => { if raw { let data = reader.read(4)?; return Ok(SQLDataCell::raw(Some(&data))); } - Ok(SQLDataCell { - tag: CellTag::Float8, - value: CellValue { - float8: f32::from_bits(reader.int::()?) as f64, - }, - ..Default::default() - }) + Ok(SQLDataCell::float8( + f32::from_bits(reader.int::()?) as f64 + )) } FieldType::MYSQL_TYPE_DOUBLE => { if raw { let data = reader.read(8)?; return Ok(SQLDataCell::raw(Some(&data))); } - Ok(SQLDataCell { - tag: CellTag::Float8, - value: CellValue { - float8: f64::from_bits(reader.int::()?), - }, - ..Default::default() - }) + Ok(SQLDataCell::float8(f64::from_bits(reader.int::()?))) } FieldType::MYSQL_TYPE_TIME => { match reader.byte()? { - 0 => { - let slice = b"00:00:00"; - Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: clone_utf8_wtf_impl(slice), - }, - free_value: 1, - ..Default::default() - }) - } + 0 => Ok(SQLDataCell::string(b"00:00:00")), l @ (8 | 12) => { let data = reader.read(l as usize)?; let time = Time::from_data(&data)?; @@ -278,18 +165,7 @@ pub fn decode_binary_value( break 'brk &buffer[..32 - remaining]; }; // reshaped for borrowck — compute remaining before re-borrowing buffer - Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(slice)) } _ => Err(bun_core::err!("InvalidBinaryValue")), } @@ -300,11 +176,7 @@ pub fn decode_binary_value( // A zero-length binary DATETIME is MySQL's "0000-00-00 00:00:00" // sentinel — surface it as Invalid Date (NaN), not the Unix epoch, // so it agrees with the text path's from_text(). - 0 => Ok(SQLDataCell { - tag: CellTag::Date, - value: CellValue { date: f64::NAN }, - ..Default::default() - }), + 0 => Ok(SQLDataCell::date(f64::NAN)), l @ (11 | 7 | 4) => { let data = reader.read(l as usize)?; let time = DateTime::from_data(&data)?; @@ -315,11 +187,7 @@ pub fn decode_binary_value( bun_jsc::JsError::Terminated => bun_core::err!("Terminated"), bun_jsc::JsError::Thrown => bun_core::err!("Thrown"), })?; - Ok(SQLDataCell { - tag: CellTag::Date, - value: CellValue { date: ts }, - ..Default::default() - }) + Ok(SQLDataCell::date(ts)) } _ => Err(bun_core::err!("InvalidBinaryValue")), }, @@ -334,19 +202,7 @@ pub fn decode_binary_value( return Ok(SQLDataCell::raw(Some(&data))); } let string_data = reader.encode_len_string()?; - let slice = string_data.slice(); - Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(string_data.slice())) } // When the column contains a binary string we return a Buffer otherwise a string @@ -372,19 +228,7 @@ pub fn decode_binary_value( if binary && character_set == BINARY_CHARSET { return Ok(SQLDataCell::raw(Some(&string_data))); } - let slice = string_data.slice(); - Ok(SQLDataCell { - tag: CellTag::String, - value: CellValue { - string: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(string_data.slice())) } FieldType::MYSQL_TYPE_JSON => { @@ -393,36 +237,14 @@ pub fn decode_binary_value( return Ok(SQLDataCell::raw(Some(&data))); } let string_data = reader.encode_len_string()?; - let slice = string_data.slice(); - Ok(SQLDataCell { - tag: CellTag::Json, - value: CellValue { - json: if !slice.is_empty() { - clone_utf8_wtf_impl(slice) - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::json(string_data.slice())) } FieldType::MYSQL_TYPE_BIT => { // BIT(1) is a special case, it's a boolean if column_length == 1 { let data = reader.encode_len_string()?; let slice = data.slice(); - Ok(SQLDataCell { - tag: CellTag::Bool, - value: CellValue { - bool_: if !slice.is_empty() && slice[0] == 1 { - 1 - } else { - 0 - }, - }, - ..Default::default() - }) + Ok(SQLDataCell::bool_(!slice.is_empty() && slice[0] == 1)) } else { let data = reader.encode_len_string()?; Ok(SQLDataCell::raw(Some(&data))) @@ -434,9 +256,3 @@ pub fn decode_binary_value( } } } - -// `leak_wtf_impl()` transfers the +1 ref to the cell (`free_value = 1`). -#[inline] -fn clone_utf8_wtf_impl(slice: &[u8]) -> bun_core::WTFStringImpl { - bun_core::String::clone_utf8(slice).leak_wtf_impl() -} diff --git a/src/sql_jsc/mysql/protocol/ResultSet.rs b/src/sql_jsc/mysql/protocol/ResultSet.rs index effb6fd88975..08704d5883d1 100644 --- a/src/sql_jsc/mysql/protocol/ResultSet.rs +++ b/src/sql_jsc/mysql/protocol/ResultSet.rs @@ -1,8 +1,5 @@ -use core::ptr; - -use crate::jsc::{ExternColumnIdentifier, JSGlobalObject, JSValue}; +use crate::jsc::{JSGlobalObject, JSValue}; use crate::mysql::my_sql_value::DateTime; -use bun_core::String as BunString; use bun_core::parse_int; use bun_sql::mysql::protocol::ColumnDefinition41; @@ -15,7 +12,7 @@ use bun_sql::shared::Data; use bun_sql::shared::SQLQueryResultMode; use crate::shared::CachedStructure; -use crate::shared::sql_data_cell::{Flags as SQLDataCellFlags, SQLDataCell, Tag, Value}; +use crate::shared::sql_data_cell::{Flags as SQLDataCellFlags, SQLDataCell}; use super::decode_binary_value::{self, decode_binary_value}; @@ -44,25 +41,14 @@ impl<'a> Row<'a> { // Passed by ref because CachedStructure is non-Copy (owns Strong + Box). cached_structure: Option<&CachedStructure>, ) -> crate::jsc::JsResult { - let mut names: *mut ExternColumnIdentifier = ptr::null_mut(); - let mut names_count: u32 = 0; - if let Some(c) = cached_structure { - if let Some(f) = c.fields.as_deref() { - names = f.as_ptr().cast_mut(); - names_count = f.len() as u32; - } - } - - SQLDataCell::construct_object_from_data_cell( + SQLDataCell::to_js_object( global_object, array, structure, - self.values.as_mut_ptr(), - self.values.len() as u32, + self.values.as_mut(), flags, result_mode as u8, - names, - names_count, + cached_structure, ) } @@ -93,143 +79,71 @@ impl<'a> Row<'a> { match column.column_type { MYSQL_TYPE_FLOAT | MYSQL_TYPE_DOUBLE => { let val: f64 = bun_core::parse_double(value.slice()).unwrap_or(f64::NAN); - *cell = SQLDataCell { - tag: Tag::Float8, - value: Value { float8: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::float8(val); } // YEAR arrives as a bare ASCII integer in the text protocol; parse it // like SHORT so `.simple()` returns the same JS number as the binary path. MYSQL_TYPE_TINY | MYSQL_TYPE_SHORT | MYSQL_TYPE_YEAR => { if column.flags.contains(ColumnFlags::UNSIGNED) { let val: u16 = parse_int::(value.slice(), 10).unwrap_or(0); - *cell = SQLDataCell { - tag: Tag::Uint4, - value: Value { uint4: val as u32 }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::uint4(val as u32); } else { let val: i16 = parse_int::(value.slice(), 10).unwrap_or(0); - *cell = SQLDataCell { - tag: Tag::Int4, - value: Value { int4: val as i32 }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::int4(val as i32); } } MYSQL_TYPE_LONG => { if column.flags.contains(ColumnFlags::UNSIGNED) { let val: u32 = parse_int::(value.slice(), 10).unwrap_or(0); - *cell = SQLDataCell { - tag: Tag::Uint4, - value: Value { uint4: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::uint4(val); } else { let val: i32 = parse_int::(value.slice(), 10).unwrap_or(i32::MIN); - *cell = SQLDataCell { - tag: Tag::Int4, - value: Value { int4: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::int4(val); } } MYSQL_TYPE_INT24 => { if column.flags.contains(ColumnFlags::UNSIGNED) { let val: u32 = parse_int::(value.slice(), 10).unwrap_or(0); - *cell = SQLDataCell { - tag: Tag::Uint4, - value: Value { uint4: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::uint4(val); } else { // -8_388_608 is the minimum value of a signed 24-bit int let val: i32 = parse_int::(value.slice(), 10).unwrap_or(-8_388_608); - *cell = SQLDataCell { - tag: Tag::Int4, - value: Value { int4: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::int4(val); } } MYSQL_TYPE_LONGLONG => { if column.flags.contains(ColumnFlags::UNSIGNED) { let val: u64 = parse_int::(value.slice(), 10).unwrap_or(0); if val <= u32::MAX as u64 { - *cell = SQLDataCell { - tag: Tag::Uint4, - value: Value { - uint4: u32::try_from(val).expect("int cast"), - }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::uint4(u32::try_from(val).expect("int cast")); return; } if self.bigint { - *cell = SQLDataCell { - tag: Tag::Uint8, - value: Value { uint8: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::uint8(val); return; } } else { let val: i64 = parse_int::(value.slice(), 10).unwrap_or(0); if val >= i32::MIN as i64 && val <= i32::MAX as i64 { - *cell = SQLDataCell { - tag: Tag::Int4, - value: Value { - int4: i32::try_from(val).expect("int cast"), - }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::int4(i32::try_from(val).expect("int cast")); return; } if self.bigint { - *cell = SQLDataCell { - tag: Tag::Int8, - value: Value { int8: val }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::int8(val); return; } } - let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::String, - value: Value { - string: clone_wtf_string_or_null(slice), - }, - free_value: 1, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::string(value.slice()); } MYSQL_TYPE_JSON => { - let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::Json, - value: Value { - json: clone_wtf_string_or_null(slice), - }, - free_value: 1, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::json(value.slice()); } MYSQL_TYPE_TIME => { // lets handle TIME special case as string // -838:59:50 to 838:59:59 is valid - let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::String, - value: Value { - string: clone_wtf_string_or_null(slice), - }, - free_value: 1, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::string(value.slice()); } MYSQL_TYPE_DATE | MYSQL_TYPE_DATETIME | MYSQL_TYPE_TIMESTAMP => { // MySQL's DATE/DATETIME/TIMESTAMP text has no timezone, so parse @@ -242,42 +156,20 @@ impl<'a> Row<'a> { Some(dt) => dt.to_js_timestamp(self.global_object).unwrap_or(f64::NAN), None => f64::NAN, }; - *cell = SQLDataCell { - tag: Tag::Date, - value: Value { date }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::date(date); } // NEWDECIMAL is always sent as an ASCII decimal string regardless of the // column's BINARY flag / charset. Computed decimals (SUM/AVG/arithmetic/CAST) // carry the BINARY flag and charset 63, so the catch-all arm's binary-charset // heuristic would wrongly return them as a Buffer. MYSQL_TYPE_NEWDECIMAL => { - let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::String, - value: Value { - string: clone_wtf_string_or_null(slice), - }, - free_value: 1, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::string(value.slice()); } MYSQL_TYPE_BIT => { // BIT(1) is a special case, it's a boolean if column.column_length == 1 { let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::Bool, - value: Value { - bool_: if !slice.is_empty() && slice[0] == 1 { - 1 - } else { - 0 - }, - }, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::bool_(!slice.is_empty() && slice[0] == 1); } else { *cell = SQLDataCell::raw(value); } @@ -292,15 +184,7 @@ impl<'a> Row<'a> { { *cell = SQLDataCell::raw(value); } else { - let slice = value.slice(); - *cell = SQLDataCell { - tag: Tag::String, - value: Value { - string: clone_wtf_string_or_null(slice), - }, - free_value: 1, - ..SQLDataCell::default() - }; + *cell = SQLDataCell::string(value.slice()); } } } @@ -310,15 +194,7 @@ impl<'a> Row<'a> { &mut self, reader: NewReader, ) -> Result<(), AnyMySQLError> { - let cells = vec![ - SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..SQLDataCell::default() - }; - self.columns.len() - ] - .into_boxed_slice(); + let cells = vec![SQLDataCell::null(); self.columns.len()].into_boxed_slice(); let mut cells = scopeguard::guard(cells, |mut cells| { for value in cells.iter_mut() { value.deinit(); @@ -336,11 +212,7 @@ impl<'a> Row<'a> { // NULL value reader.skip(result.bytes_read); // this dont matter if is raw because we will sent as null too like in postgres - *value = SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..SQLDataCell::default() - }; + *value = SQLDataCell::null(); } else { if self.raw { let data = reader.encode_len_string()?; @@ -382,15 +254,7 @@ impl<'a> Row<'a> { let bitmap_bytes = (self.columns.len() + 7 + 2) / 8; let null_bitmap = reader.read(bitmap_bytes)?; - let cells = vec![ - SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..SQLDataCell::default() - }; - self.columns.len() - ] - .into_boxed_slice(); + let cells = vec![SQLDataCell::null(); self.columns.len()].into_boxed_slice(); let mut cells = scopeguard::guard(cells, |mut cells| { for value in cells.iter_mut() { value.deinit(); @@ -404,27 +268,22 @@ impl<'a> Row<'a> { let bit_pos = ((bitmap_offset + i) & 7) as u8; let is_null = (null_bitmap.slice()[byte_pos] & (1u8 << bit_pos)) != 0; + let column = &self.columns[i]; if is_null { - *value = SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..SQLDataCell::default() - }; - continue; + *value = SQLDataCell::null(); + } else { + *value = decode_binary_value( + self.global_object, + column.column_type, + column.column_length, + self.raw, + self.bigint, + column.flags.contains(ColumnFlags::UNSIGNED), + column.flags.contains(ColumnFlags::BINARY), + column.character_set, + reader, + )?; } - - let column = &self.columns[i]; - *value = decode_binary_value( - self.global_object, - column.column_type, - column.column_length, - self.raw, - self.bigint, - column.flags.contains(ColumnFlags::UNSIGNED), - column.flags.contains(ColumnFlags::BINARY), - column.character_set, - reader, - )?; value.index = match column.name_or_index { // The indexed columns can be out of order. NameOrIndex::Index(idx) => idx, @@ -461,16 +320,3 @@ impl<'a> Drop for Row<'a> { // self.columns is intentionally left out. } } - -// ─── helpers ────────────────────────────────────────────────────────────── - -#[inline] -fn clone_wtf_string_or_null(slice: &[u8]) -> bun_core::WTFStringImpl { - // Extracts the raw WTFStringImpl* from a freshly-cloned string (ownership transferred to the cell, - // freed via `free_value = 1`). - if !slice.is_empty() { - BunString::clone_utf8(slice).leak_wtf_impl() - } else { - ptr::null_mut() - } -} diff --git a/src/sql_jsc/postgres/DataCell.rs b/src/sql_jsc/postgres/DataCell.rs index 054410695bd8..0eee5e1ca5c0 100644 --- a/src/sql_jsc/postgres/DataCell.rs +++ b/src/sql_jsc/postgres/DataCell.rs @@ -129,11 +129,7 @@ fn parse_array( return Ok(SQLDataCell { tag: Tag::Array, value: Value { - array: Array { - ptr: core::ptr::null_mut(), - len: 0, - cap: 0, - }, + array: Array::default(), }, ..Default::default() }); @@ -220,14 +216,10 @@ fn parse_array( let date_str = &slice[1..current_idx]; let mut str = BunString::init(date_str); // defer str.deref() → Drop on BunString - array.push(SQLDataCell { - tag: Tag::Date, - value: Value { - date: crate::jsc::bun_string_jsc::parse_date(&mut str, global_object) - .map_err(crate::jsc::js_error_to_postgres)?, - }, - ..Default::default() - }); + array.push(SQLDataCell::date( + crate::jsc::bun_string_jsc::parse_date(&mut str, global_object) + .map_err(crate::jsc::js_error_to_postgres)?, + )); slice = try_slice(slice, current_idx + 1); continue; @@ -244,18 +236,7 @@ fn parse_array( }; let unescaped = unescape_postgres_string(str_bytes, buffer) .map_err(|_| AnyPostgresError::InvalidByteSequence)?; - array.push(SQLDataCell { - tag: Tag::Json, - value: Value { - json: if !unescaped.is_empty() { - BunString::clone_utf8(unescaped).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::json(unescaped)); slice = try_slice(slice, current_idx + 1); continue; } @@ -264,14 +245,7 @@ fn parse_array( let str_bytes = &slice[1..current_idx]; if str_bytes.is_empty() { // empty string - array.push(SQLDataCell { - tag: Tag::String, - value: Value { - string: core::ptr::null_mut(), - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::string(b"")); slice = try_slice(slice, current_idx + 1); continue; } @@ -285,18 +259,7 @@ fn parse_array( }; let string_bytes = unescape_postgres_string(str_bytes, buffer) .map_err(|_| AnyPostgresError::InvalidByteSequence)?; - array.push(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !string_bytes.is_empty() { - BunString::clone_utf8(string_bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::string(string_bytes)); slice = try_slice(slice, current_idx + 1); continue; @@ -352,45 +315,22 @@ fn parse_array( let element = &slice[0..current_idx]; // lets handle NULL case here, if is a string "NULL" it will have quotes, if its a NULL it will be just NULL if element == b"NULL" { - array.push(SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - }); + array.push(SQLDataCell::null()); slice = try_slice(slice, current_idx); continue; } if array_type == types::Tag::date_array { let mut str = BunString::init(element); - array.push(SQLDataCell { - tag: Tag::Date, - value: Value { date: crate::jsc::bun_string_jsc::parse_date(&mut str, global_object).map_err(crate::jsc::js_error_to_postgres)? }, - ..Default::default() - }); + array.push(SQLDataCell::date( + crate::jsc::bun_string_jsc::parse_date(&mut str, global_object) + .map_err(crate::jsc::js_error_to_postgres)?, + )); } else { // the only escape sequency possible here is \b if element == b"\\b" { - array.push(SQLDataCell { - tag: Tag::String, - value: Value { - string: BunString::clone_utf8(b"\x08").leak_wtf_impl(), - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::string(b"\x08")); } else { - array.push(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !element.is_empty() { - BunString::clone_utf8(element).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::string(element)); } } slice = try_slice(slice, current_idx); @@ -406,21 +346,13 @@ fn parse_array( } if slice.len() >= 4 { if &slice[0..4] == b"NULL" { - array.push(SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - }); + array.push(SQLDataCell::null()); slice = try_slice(slice, 4); continue; } } if &slice[0..3] == b"NaN" { - array.push(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: f64::NAN }, - ..Default::default() - }); + array.push(SQLDataCell::float8(f64::NAN)); slice = try_slice(slice, 3); continue; } @@ -433,21 +365,13 @@ fn parse_array( return Err(AnyPostgresError::UnsupportedArrayFormat); } if &slice[0..5] == b"false" { - array.push(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: 0 }, - ..Default::default() - }); + array.push(SQLDataCell::bool_(false)); slice = try_slice(slice, 5); continue; } return Err(AnyPostgresError::UnsupportedArrayFormat); } else { - array.push(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: 0 }, - ..Default::default() - }); + array.push(SQLDataCell::bool_(false)); slice = try_slice(slice, 1); continue; } @@ -459,21 +383,13 @@ fn parse_array( return Err(AnyPostgresError::UnsupportedArrayFormat); } if &slice[0..4] == b"true" { - array.push(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: 1 }, - ..Default::default() - }); + array.push(SQLDataCell::bool_(true)); slice = try_slice(slice, 4); continue; } return Err(AnyPostgresError::UnsupportedArrayFormat); } else { - array.push(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: 1 }, - ..Default::default() - }); + array.push(SQLDataCell::bool_(true)); slice = try_slice(slice, 1); continue; } @@ -485,17 +401,9 @@ fn parse_array( array_type, types::Tag::date_array | types::Tag::timestamp_array | types::Tag::timestamptz_array ) { - array.push(SQLDataCell { - tag: Tag::Date, - value: Value { date: f64::INFINITY }, - ..Default::default() - }); + array.push(SQLDataCell::date(f64::INFINITY)); } else { - array.push(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: f64::INFINITY }, - ..Default::default() - }); + array.push(SQLDataCell::float8(f64::INFINITY)); } slice = try_slice(slice, 8); continue; @@ -582,17 +490,9 @@ fn parse_array( | types::Tag::timestamp_array | types::Tag::timestamptz_array ) { - array.push(SQLDataCell { - tag: Tag::Date, - value: Value { date: val }, - ..Default::default() - }); + array.push(SQLDataCell::date(val)); } else { - array.push(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: val }, - ..Default::default() - }); + array.push(SQLDataCell::float8(val)); } advance_after = Some(8 + (is_negative as usize)); break; @@ -616,52 +516,29 @@ fn parse_array( } let element = &slice[0..current_idx]; if is_float || array_type == types::Tag::float8_array { - array.push(SQLDataCell { - tag: Tag::Float8, - value: Value { - float8: bun_core::parse_double(element).unwrap_or(f64::NAN), - }, - ..Default::default() - }); + array.push(SQLDataCell::float8( + bun_core::parse_double(element).unwrap_or(f64::NAN), + )); slice = try_slice(slice, current_idx); continue; } match array_type { types::Tag::int8_array => { if bigint { - array.push(SQLDataCell { - tag: Tag::Int8, - value: Value { - int8: bun_core::fmt::parse_decimal::(element) - .ok_or(AnyPostgresError::UnsupportedArrayFormat)?, - }, - ..Default::default() - }); + array.push(SQLDataCell::int8( + bun_core::fmt::parse_decimal::(element) + .ok_or(AnyPostgresError::UnsupportedArrayFormat)?, + )); } else { - array.push(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !element.is_empty() { - BunString::clone_utf8(element).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }); + array.push(SQLDataCell::string(element)); } slice = try_slice(slice, current_idx); continue; } types::Tag::cid_array | types::Tag::xid_array | types::Tag::oid_array => { - array.push(SQLDataCell { - tag: Tag::Uint4, - value: Value { - uint4: bun_core::fmt::parse_decimal::(element).unwrap_or(0), - }, - ..Default::default() - }); + array.push(SQLDataCell::uint4( + bun_core::fmt::parse_decimal::(element).unwrap_or(0), + )); slice = try_slice(slice, current_idx); continue; } @@ -669,13 +546,7 @@ fn parse_array( let value = bun_core::fmt::parse_decimal::(element) .ok_or(AnyPostgresError::UnsupportedArrayFormat)?; - array.push(SQLDataCell { - tag: Tag::Int4, - value: Value { - int4: value, - }, - ..Default::default() - }); + array.push(SQLDataCell::int4(value)); slice = try_slice(slice, current_idx); continue; } @@ -870,103 +741,58 @@ pub(crate) fn from_bytes( } T::int2 => { if binary { - Ok(SQLDataCell { - tag: Tag::Int4, - value: Value { int4: parse_binary_int2(bytes)? as i32 }, - ..Default::default() - }) + Ok(SQLDataCell::int4(parse_binary_int2(bytes)? as i32)) } else { - Ok(SQLDataCell { - tag: Tag::Int4, - value: Value { int4: bun_core::fmt::parse_decimal::(bytes).unwrap_or(0) }, - ..Default::default() - }) + Ok(SQLDataCell::int4( + bun_core::fmt::parse_decimal::(bytes).unwrap_or(0), + )) } } T::cid | T::xid | T::oid => { if binary { - Ok(SQLDataCell { - tag: Tag::Uint4, - value: Value { uint4: parse_binary_oid(bytes)? }, - ..Default::default() - }) + Ok(SQLDataCell::uint4(parse_binary_oid(bytes)?)) } else { - Ok(SQLDataCell { - tag: Tag::Uint4, - value: Value { uint4: bun_core::fmt::parse_decimal::(bytes).unwrap_or(0) }, - ..Default::default() - }) + Ok(SQLDataCell::uint4( + bun_core::fmt::parse_decimal::(bytes).unwrap_or(0), + )) } } T::int4 => { if binary { - Ok(SQLDataCell { - tag: Tag::Int4, - value: Value { int4: parse_binary_int4(bytes)? }, - ..Default::default() - }) + Ok(SQLDataCell::int4(parse_binary_int4(bytes)?)) } else { - Ok(SQLDataCell { - tag: Tag::Int4, - value: Value { int4: bun_core::fmt::parse_decimal::(bytes).unwrap_or(0) }, - ..Default::default() - }) + Ok(SQLDataCell::int4( + bun_core::fmt::parse_decimal::(bytes).unwrap_or(0), + )) } } // postgres when reading bigint as int8 it returns a string unless type: { bigint: postgres.BigInt is set T::int8 => { if bigint { // .int8 is a 64-bit integer always string - Ok(SQLDataCell { - tag: Tag::Int8, - value: Value { int8: bun_core::fmt::parse_decimal::(bytes).unwrap_or(0) }, - ..Default::default() - }) + Ok(SQLDataCell::int8( + bun_core::fmt::parse_decimal::(bytes).unwrap_or(0), + )) } else { - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !bytes.is_empty() { - BunString::clone_utf8(bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(bytes)) } } T::float8 => { if binary && bytes.len() == 8 { - Ok(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: parse_binary_float8(bytes)? }, - ..Default::default() - }) + Ok(SQLDataCell::float8(parse_binary_float8(bytes)?)) } else { - let float8: f64 = bun_core::parse_double(bytes).unwrap_or(f64::NAN); - Ok(SQLDataCell { - tag: Tag::Float8, - value: Value { float8 }, - ..Default::default() - }) + Ok(SQLDataCell::float8( + bun_core::parse_double(bytes).unwrap_or(f64::NAN), + )) } } T::float4 => { if binary && bytes.len() == 4 { - Ok(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: parse_binary_float4(bytes)? as f64 }, - ..Default::default() - }) + Ok(SQLDataCell::float8(parse_binary_float4(bytes)? as f64)) } else { - let float4: f64 = bun_core::parse_double(bytes).unwrap_or(f64::NAN); - Ok(SQLDataCell { - tag: Tag::Float8, - value: Value { float8: float4 }, - ..Default::default() - }) + Ok(SQLDataCell::float8( + bun_core::parse_double(bytes).unwrap_or(f64::NAN), + )) } } T::numeric => { @@ -977,86 +803,37 @@ pub(crate) fn from_bytes( // if is binary format lets display as a string because JS cant handle it in a safe way let result = parse_binary_numeric(bytes, &mut numeric_buffer) .map_err(|_| AnyPostgresError::UnsupportedNumericFormat)?; - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: BunString::clone_utf8(result.slice()).leak_wtf_impl(), - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(result.slice())) } else { // nice text is actually what we want here - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !bytes.is_empty() { - BunString::clone_utf8(bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(bytes)) } } - T::jsonb | T::json => Ok(SQLDataCell { - tag: Tag::Json, - value: Value { - json: if !bytes.is_empty() { - BunString::clone_utf8(bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }), + T::jsonb | T::json => Ok(SQLDataCell::json(bytes)), T::bool => { if binary { - Ok(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: (!bytes.is_empty() && bytes[0] == 1) as u8 }, - ..Default::default() - }) + Ok(SQLDataCell::bool_(!bytes.is_empty() && bytes[0] == 1)) } else { - Ok(SQLDataCell { - tag: Tag::Bool, - value: Value { bool_: (!bytes.is_empty() && bytes[0] == b't') as u8 }, - ..Default::default() - }) + Ok(SQLDataCell::bool_(!bytes.is_empty() && bytes[0] == b't')) } } tag @ (T::date | T::timestamp | T::timestamptz) => { if bytes.is_empty() { - return Ok(SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - }); + return Ok(SQLDataCell::null()); } if binary && bytes.len() == 8 { match tag { - T::timestamptz => Ok(SQLDataCell { - tag: Tag::DateWithTimeZone, - value: Value { date_with_time_zone: crate::postgres::types::date::from_binary(bytes) }, - ..Default::default() - }), - T::timestamp => Ok(SQLDataCell { - tag: Tag::Date, - value: Value { date: crate::postgres::types::date::from_binary(bytes) }, - ..Default::default() - }), + T::timestamptz => Ok(SQLDataCell::date_with_tz( + crate::postgres::types::date::from_binary(bytes), + )), + T::timestamp => Ok(SQLDataCell::date( + crate::postgres::types::date::from_binary(bytes), + )), _ => unreachable!(), } } else { if bun_core::strings::eql_case_insensitive_ascii(bytes, b"NULL", true) { - return Ok(SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - }); + return Ok(SQLDataCell::null()); } // `timestamp` (without time zone) text carries no offset, so // decode its components as UTC to match the binary path. `date` @@ -1074,20 +851,12 @@ pub(crate) fn from_bytes( .map_err(crate::jsc::js_error_to_postgres)? } }; - Ok(SQLDataCell { - tag: Tag::Date, - value: Value { date }, - ..Default::default() - }) + Ok(SQLDataCell::date(date)) } } tag @ (T::time | T::timetz) => { if bytes.is_empty() { - return Ok(SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - }); + return Ok(SQLDataCell::null()); } if binary { if tag == T::time && bytes.len() == 8 { @@ -1098,14 +867,7 @@ pub(crate) fn from_bytes( let mut buffer = [0u8; 32]; let len = Postgres__formatTime(microseconds, &mut buffer, 32); - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: BunString::clone_utf8(&buffer[0..len]).leak_wtf_impl(), - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(&buffer[0..len])) } else if tag == T::timetz && bytes.len() == 12 { // PostgreSQL sends timetz as microseconds since midnight (8 bytes) + timezone offset in seconds (4 bytes) let microseconds = i64::from_ne_bytes(bytes[0..8].try_into().expect("infallible: size matches")).swap_bytes(); @@ -1115,31 +877,13 @@ pub(crate) fn from_bytes( let mut buffer = [0u8; 48]; let len = Postgres__formatTimeTz(microseconds, tz_offset_seconds, &mut buffer, 48); - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: BunString::clone_utf8(&buffer[0..len]).leak_wtf_impl(), - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(&buffer[0..len])) } else { Err(AnyPostgresError::InvalidBinaryData) } } else { // Text format - just return as string - Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !bytes.is_empty() { - BunString::clone_utf8(bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }) + Ok(SQLDataCell::string(bytes)) } } @@ -1205,18 +949,7 @@ pub(crate) fn from_bytes( | T::timestamp_array | T::timestamptz_array | T::interval_array) => parse_array(bytes, bigint, tag, global_object, None, false, 0), - _ => Ok(SQLDataCell { - tag: Tag::String, - value: Value { - string: if !bytes.is_empty() { - BunString::clone_utf8(bytes).leak_wtf_impl() - } else { - core::ptr::null_mut() - }, - }, - free_value: 1, - ..Default::default() - }), + _ => Ok(SQLDataCell::string(bytes)), } } @@ -1465,25 +1198,14 @@ impl<'a> Putter<'a> { result_mode: PostgresSQLQueryResultMode, cached_structure: Option<&PostgresCachedStructure>, ) -> Result { - let mut names: *mut crate::jsc::ExternColumnIdentifier = core::ptr::null_mut(); - let mut names_count: u32 = 0; - if let Some(c) = cached_structure { - if let Some(f) = c.fields.as_ref() { - names = f.as_ptr().cast_mut(); - names_count = f.len() as u32; - } - } - - SQLDataCell::construct_object_from_data_cell( + SQLDataCell::to_js_object( global_object, array, structure, - self.list.as_mut_ptr(), - self.fields.len() as u32, + self.list, flags, result_mode as u8, - names, - names_count, + cached_structure, ) .map_err(crate::jsc::js_error_to_postgres) } @@ -1536,11 +1258,7 @@ impl<'a> Putter<'a> { self.global_object, )? } else { - SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - } + SQLDataCell::null() }; } self.count += 1; diff --git a/src/sql_jsc/shared/SQLDataCell.rs b/src/sql_jsc/shared/SQLDataCell.rs index 225f3244ec0b..ccf584df3ff8 100644 --- a/src/sql_jsc/shared/SQLDataCell.rs +++ b/src/sql_jsc/shared/SQLDataCell.rs @@ -278,6 +278,115 @@ impl SQLDataCell { } } + #[inline] + pub fn null() -> SQLDataCell { + SQLDataCell::default() + } + + #[inline] + pub fn int4(value: i32) -> SQLDataCell { + SQLDataCell { + tag: Tag::Int4, + value: Value { int4: value }, + ..Default::default() + } + } + + #[inline] + pub fn uint4(value: u32) -> SQLDataCell { + SQLDataCell { + tag: Tag::Uint4, + value: Value { uint4: value }, + ..Default::default() + } + } + + #[inline] + pub fn int8(value: i64) -> SQLDataCell { + SQLDataCell { + tag: Tag::Int8, + value: Value { int8: value }, + ..Default::default() + } + } + + #[inline] + pub fn uint8(value: u64) -> SQLDataCell { + SQLDataCell { + tag: Tag::Uint8, + value: Value { uint8: value }, + ..Default::default() + } + } + + #[inline] + pub fn float8(value: f64) -> SQLDataCell { + SQLDataCell { + tag: Tag::Float8, + value: Value { float8: value }, + ..Default::default() + } + } + + #[inline] + pub fn bool_(value: bool) -> SQLDataCell { + SQLDataCell { + tag: Tag::Bool, + value: Value { bool_: value as u8 }, + ..Default::default() + } + } + + #[inline] + pub fn date(value: f64) -> SQLDataCell { + SQLDataCell { + tag: Tag::Date, + value: Value { date: value }, + ..Default::default() + } + } + + #[inline] + pub fn date_with_tz(value: f64) -> SQLDataCell { + SQLDataCell { + tag: Tag::DateWithTimeZone, + value: Value { + date_with_time_zone: value, + }, + ..Default::default() + } + } + + /// Owned string cell: clones `bytes` into a WTFStringImpl, freed via + /// `free_value = 1`. Empty input becomes a null pointer, which the C++ + /// side (SQLClient.cpp) renders as the empty string. + #[inline] + pub fn string(bytes: &[u8]) -> SQLDataCell { + SQLDataCell { + tag: Tag::String, + value: Value { + string: clone_utf8_or_null(bytes), + }, + free_value: 1, + ..Default::default() + } + } + + /// Owned JSON cell: clones `bytes` into a WTFStringImpl, freed via + /// `free_value = 1`. Empty input becomes a null pointer, which the C++ + /// side (SQLClient.cpp) renders as `null`. + #[inline] + pub fn json(bytes: &[u8]) -> SQLDataCell { + SQLDataCell { + tag: Tag::Json, + value: Value { + json: clone_utf8_or_null(bytes), + }, + free_value: 1, + ..Default::default() + } + } + pub fn raw<'a>(optional_bytes: impl IntoOptionalData<'a>) -> SQLDataCell { if let Some(bytes) = optional_bytes.into_optional_data() { let bytes_slice = bytes.slice(); @@ -293,11 +402,37 @@ impl SQLDataCell { }; } // TODO: check empty and null fields - SQLDataCell { - tag: Tag::Null, - value: Value { null: 0 }, - ..Default::default() - } + SQLDataCell::null() + } + + /// Shared wrapper around `construct_object_from_data_cell` used by the + /// per-row `to_js` paths (postgres `Putter`, mysql `Row`): extracts the + /// cached-structure column names and forwards the cells. + pub fn to_js_object( + global_object: &JSGlobalObject, + array: JSValue, + structure: JSValue, + cells: &mut [SQLDataCell], + flags: Flags, + result_mode: u8, + cached_structure: Option<&crate::shared::CachedStructure>, + ) -> JsResult { + let (names, names_count) = match cached_structure.and_then(|c| c.fields.as_deref()) { + Some(f) => (f.as_ptr().cast_mut(), f.len() as u32), + None => (ptr::null_mut(), 0), + }; + + SQLDataCell::construct_object_from_data_cell( + global_object, + array, + structure, + cells.as_mut_ptr(), + cells.len() as u32, + flags, + result_mode, + names, + names_count, + ) } // TODO: cppbind isn't yet able to detect slice parameters when the next is uint32_t @@ -372,6 +507,18 @@ impl<'a> IntoOptionalData<'a> for Option<&'a mut Data> { } } +/// Clones the bytes into a fresh `WTFStringImpl` whose +1 ref is transferred +/// to the cell (`free_value = 1`). Empty input maps to a null pointer instead +/// of allocating an empty string. +#[inline] +fn clone_utf8_or_null(bytes: &[u8]) -> WTFStringImpl { + if !bytes.is_empty() { + bun_core::String::clone_utf8(bytes).leak_wtf_impl() + } else { + ptr::null_mut() + } +} + bitflags::bitflags! { #[repr(transparent)] #[derive(Copy, Clone, Default)] diff --git a/test/js/sql/sql-mysql-binary-null-indexed.test.ts b/test/js/sql/sql-mysql-binary-null-indexed.test.ts new file mode 100644 index 000000000000..7b0fa09e1c4a --- /dev/null +++ b/test/js/sql/sql-mysql-binary-null-indexed.test.ts @@ -0,0 +1,34 @@ +// The binary-protocol row decoder skipped the `index` / `is_indexed_column` +// assignments for cells marked NULL in the null bitmap (it `continue;`d out +// of the loop right after writing the null cell). For columns whose name is +// all digits, those fields tell SQLClient.cpp which object index to place the +// value at, so a NULL value on such a column landed at index 0 instead of the +// column's numeric name (and tripped `ASSERT(cell.isIndexedColumn())` in +// debug builds). The text-protocol decoder already handled this correctly. + +import { SQL } from "bun"; +import { expect, test } from "bun:test"; +import { describeWithContainer } from "harness"; + +describeWithContainer("mysql", { image: "mysql_plain" }, container => { + test("binary-protocol NULL in a digit-named column lands at that column's index", async () => { + await container.ready; + await using sql = new SQL({ url: `mysql://root@${container.host}:${container.port}/bun_sql_test`, max: 1 }); + + // All-digit column names make ColumnIdentifier classify them as Index(n). + // Column "2" carries a non-NULL value to prove NULL placement (not just + // presence) is what's being checked. + const expected = { "2": 42, "5": null, "7": null }; + + // Prepared → binary protocol. Before the fix the two NULL cells kept + // index=0 / is_indexed_column=0, so the indexed-only fast path in + // SQLClient.cpp wrote both nulls to slot 0 and dropped keys "5" and "7". + const [binaryRow] = await sql`SELECT NULL AS \`5\`, CAST(42 AS SIGNED) AS \`2\`, NULL AS \`7\``; + expect(binaryRow).toEqual(expected); + + // .simple() → text protocol. This path was already correct; the two + // protocols must agree. + const [textRow] = await sql`SELECT NULL AS \`5\`, CAST(42 AS SIGNED) AS \`2\`, NULL AS \`7\``.simple(); + expect(textRow).toEqual(expected); + }); +}); diff --git a/test/js/sql/sql-mysql-column-name-digits.test.ts b/test/js/sql/sql-mysql-column-name-digits.test.ts index adb1d3529fb7..68b4ee8e444a 100644 --- a/test/js/sql/sql-mysql-column-name-digits.test.ts +++ b/test/js/sql/sql-mysql-column-name-digits.test.ts @@ -14,10 +14,8 @@ // Runs against a real MySQL/MariaDB server. The classifier is shared with // Postgres, so this also covers that decoder. -import { SQL } from "bun"; -import { beforeAll, describe, expect, test } from "bun:test"; -import { existsSync } from "fs"; -import { bunEnv, bunExe, describeWithContainer, isDockerEnabled, isLinux } from "harness"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, describeWithContainer } from "harness"; import path from "path"; const fixture = path.join(import.meta.dir, "sql-mysql-column-name-digits.fixture.ts"); @@ -34,107 +32,25 @@ async function runFixture(url: string) { return { stdout, stderr, exitCode }; } -function assertFixtureOutput(stdout: string, stderr: string, exitCode: number) { - const filteredStderr = stderr - .split(/\r?\n/) - .filter(l => l && !l.startsWith("WARNING: ASAN interferes")) - .join("\n"); - expect(filteredStderr).toBe(""); - const lines = stdout.trim().split(/\r?\n/); - expect(lines[0]).toBe("CONNECTED"); - // `2024_01`/`2024_02` must be NAMED keys (not indices 202401/202402), `8` - // round-trips, and nothing is dropped. - expect(JSON.parse(lines[1] ?? "null")).toEqual({ - row: { product: "widget", "2024_01": 10, "2024_02": 20, "8": 42 }, - keys: ["2024_01", "2024_02", "8", "product"], - }); - expect(exitCode).toBe(0); -} - -if (isDockerEnabled()) { - // CI: run against the docker-compose MySQL service. - describeWithContainer("mysql", { image: "mysql_plain" }, container => { - test("a digits-with-interior-underscore column stays a named key", async () => { - await container.ready; - const url = `mysql://root@${container.host}:${container.port}/bun_sql_test`; - const { stdout, stderr, exitCode } = await runFixture(url); - assertFixtureOutput(stdout, stderr, exitCode); +describeWithContainer("mysql", { image: "mysql_plain" }, container => { + test("a digits-with-interior-underscore column stays a named key", async () => { + await container.ready; + const url = `mysql://root@${container.host}:${container.port}/bun_sql_test`; + const { stdout, stderr, exitCode } = await runFixture(url); + + const filteredStderr = stderr + .split(/\r?\n/) + .filter(l => l && !l.startsWith("WARNING: ASAN interferes")) + .join("\n"); + expect(filteredStderr).toBe(""); + const lines = stdout.trim().split(/\r?\n/); + expect(lines[0]).toBe("CONNECTED"); + // `2024_01`/`2024_02` must be NAMED keys (not indices 202401/202402), `8` + // round-trips, and nothing is dropped. + expect(JSON.parse(lines[1] ?? "null")).toEqual({ + row: { product: "widget", "2024_01": 10, "2024_02": 20, "8": 42 }, + keys: ["2024_01", "2024_02", "8", "product"], }); + expect(exitCode).toBe(0); }); -} else { - // No docker daemon (e.g. the sandboxed dev/CI-gate container, which ships a - // native MariaDB). Connect to that real server: start it if needed, provision - // a passwordless TCP user over the root unix socket, and run the fixture - // against it. `MYSQL_URL` short-circuits all of this when set. - const MYSQL_SOCKET = "/run/mysqld/mysqld.sock"; - - async function waitForSocket(timeoutMs: number): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - if (existsSync(MYSQL_SOCKET)) return true; - await Bun.sleep(250); - } - return existsSync(MYSQL_SOCKET); - } - - // Start the native MariaDB if its socket isn't already there. Best-effort: - // on environments without the binaries / permissions this just fails and the - // test skips. - async function ensureServerStarted(): Promise { - if (existsSync(MYSQL_SOCKET)) return true; - if (Bun.which("mysqld_safe") == null) return false; - Bun.spawn({ - cmd: ["mysqld_safe", "--user=mysql", "--datadir=/var/lib/mysql"], - stdout: "ignore", - stderr: "ignore", - stdin: "ignore", - timeout: 60_000, - }).unref(); - return waitForSocket(30_000); - } - - // Create a passwordless `bun_sql_test` user reachable over TCP. root uses - // unix_socket auth (no TCP), so provision through the socket first. - async function provisionTcpUser(): Promise { - await using root = new SQL({ adapter: "mysql", username: "root", database: "mysql", path: MYSQL_SOCKET, max: 1 }); - await root`CREATE DATABASE IF NOT EXISTS bun_sql_test`; - await root.unsafe("CREATE USER IF NOT EXISTS 'bun_sql_test'@'%' IDENTIFIED BY ''"); - await root.unsafe("CREATE USER IF NOT EXISTS 'bun_sql_test'@'localhost' IDENTIFIED BY ''"); - await root.unsafe("GRANT ALL PRIVILEGES ON *.* TO 'bun_sql_test'@'%'"); - await root.unsafe("GRANT ALL PRIVILEGES ON *.* TO 'bun_sql_test'@'localhost'"); - await root.unsafe("FLUSH PRIVILEGES"); - } - - let url: string | null = process.env.MYSQL_URL ?? null; - - beforeAll(async () => { - if (url) return; - if (!isLinux) return; - try { - if (!(await ensureServerStarted())) return; - await provisionTcpUser(); - url = "mysql://bun_sql_test@127.0.0.1:3306/bun_sql_test"; - } catch { - // Leave url null → the test skips; the docker branch covers CI. - url = null; - } - }); - - describe("mysql (local)", () => { - test("a digits-with-interior-underscore column stays a named key", async () => { - if (!url) { - console.warn("sql-mysql-column-name-digits: no MySQL reachable in this environment; skipping assertions"); - return; - } - const { stdout, stderr, exitCode } = await runFixture(url); - // The fixture prints "CONNECTED" after the priming query succeeds. If a - // URL was resolved but the fixture never connected, that's a real error. - if (!stdout.startsWith("CONNECTED")) { - throw new Error( - `sql-mysql-column-name-digits: could not connect to ${url}\nstdout:\n${stdout}\nstderr:\n${stderr}`, - ); - } - assertFixtureOutput(stdout, stderr, exitCode); - }); - }); -} +});