Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
21 changes: 9 additions & 12 deletions docs/runtime/sql.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1185,17 +1185,15 @@ try {

### Data Type Errors

| Data Type Errors | Description |
| ------------------------------------------------------- | ------------------------------------- |
| `ERR_POSTGRES_INVALID_BINARY_DATA` | Invalid binary data format |
| `ERR_POSTGRES_INVALID_BYTE_SEQUENCE` | Invalid byte sequence |
| `ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING` | Encoding error |
| `ERR_POSTGRES_INVALID_CHARACTER` | Invalid character in data |
| `ERR_POSTGRES_OVERFLOW` | Numeric overflow |
| `ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT` | Unsupported binary format |
| `ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE` | Integer size not supported |
| `ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET` | Multidimensional arrays not supported |
| `ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET` | NULL values in arrays not supported |
| Data Type Errors | Description |
| ------------------------------------------------- | -------------------------- |
| `ERR_POSTGRES_INVALID_BINARY_DATA` | Invalid binary data format |
| `ERR_POSTGRES_INVALID_BYTE_SEQUENCE` | Invalid byte sequence |
| `ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING` | Encoding error |
| `ERR_POSTGRES_INVALID_CHARACTER` | Invalid character in data |
| `ERR_POSTGRES_OVERFLOW` | Numeric overflow |
| `ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT` | Unsupported binary format |
| `ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE` | Integer size not supported |

The client raises these errors while it decodes a result, after the server ran the query. Only that query rejects. The connection stays open, and the other queries on it continue. An `INSERT`, `UPDATE` or `DELETE ... RETURNING` can already be stored at that point, so do not retry a write blindly. To read the value, cast the column to `text` or use `.raw()`.

Expand Down Expand Up @@ -1416,7 +1414,6 @@ We also haven't implemented some of the more uncommon features like:
- GSSAPI authentication
- `SCRAM-SHA-256-PLUS` support
- Point & PostGIS types
- All the multi-dimensional integer array types (only a couple of the types are supported)

---

Expand Down
2 changes: 0 additions & 2 deletions src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,7 @@ const errors: ErrorCodeMapping = [
["ERR_POSTGRES_INVALID_SERVER_SIGNATURE", Error, "PostgresError"],
["ERR_POSTGRES_INVALID_TRANSACTION_STATE", Error, "PostgresError"],
["ERR_POSTGRES_LIFETIME_TIMEOUT", Error, "PostgresError"],
["ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET", Error, "PostgresError"],
["ERR_POSTGRES_NOT_TAGGED_CALL", Error, "PostgresError"],
["ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET", Error, "PostgresError"],
["ERR_POSTGRES_OVERFLOW", TypeError, "PostgresError"],
["ERR_POSTGRES_QUERY_CANCELLED", Error, "PostgresError"],
["ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64", Error, "PostgresError"],
Expand Down
140 changes: 65 additions & 75 deletions src/jsc/bindings/SQLClient.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
#include "JavaScriptCore/JSGlobalObjectInlines.h"
#include "JavaScriptCore/JSType.h"
#include "JavaScriptCore/TypedArrayAdaptersForwardDeclarations.h"
#include <JavaScriptCore/ObjectInitializationScope.h>
#include <bit>
#include <cmath>

namespace Bun {
using namespace JSC;
Expand Down Expand Up @@ -57,13 +60,17 @@ typedef struct DataCellRaw {
uint64_t length;
} DataCellRaw;

typedef struct TypedArrayDataCell {
void* headPtr;
void* data;
enum class PackedArrayElem : uint8_t {
Int4 = 0,
Float4 = 1,
};

// A 1-D binary int4[]/float4[] with no NULL, still in the wire buffer.
typedef struct PackedArrayDataCell {
const uint8_t* data;
uint32_t length;
uint32_t byteLength;
JSC::JSType type;
} TypedArrayDataCell;
PackedArrayElem elem;
} PackedArrayDataCell;

typedef union DataCellValue {
uint8_t null_value;
Expand All @@ -78,7 +85,7 @@ typedef union DataCellValue {
size_t bytea[2];
WTF::StringImpl* json;
DataCellArray array;
TypedArrayDataCell typed_array;
PackedArrayDataCell packed_array;
DataCellRaw raw;
uint32_t unsigned_integer;
} DataCellValue;
Expand All @@ -95,7 +102,7 @@ enum class DataCellTag : uint8_t {
Bytea = 8,
Json = 9,
Array = 10,
TypedArray = 11,
PackedArray = 11,
Raw = 12,
UnsignedInteger = 13,
UnsignedBigint = 14,
Expand All @@ -119,6 +126,10 @@ typedef struct DataCell {
bool isDuplicateColumn() const { return _indexedColumnFlag == 2; }
} DataCell;

// Rust declares the same layout (`SQLDataCell` in src/sql_jsc/shared/SQLDataCell.rs).
static_assert(sizeof(DataCell) == 32, "DataCell layout must match the Rust SQLDataCell");
static_assert(sizeof(PackedArrayDataCell) == 16, "PackedArrayDataCell layout must match the Rust PackedArray");

class BunStructureFlags {
public:
uint32_t flags;
Expand Down Expand Up @@ -218,79 +229,58 @@ static JSC::JSValue toJS(JSC::VM& vm, JSC::JSGlobalObject* globalObject, DataCel
}
RELEASE_AND_RETURN(scope, JSC::constructArray(globalObject, static_cast<ArrayAllocationProfile*>(nullptr), args));
}
case DataCellTag::TypedArray: {
JSC::JSType type = static_cast<JSC::JSType>(cell.value.typed_array.type);
uint32_t length = cell.value.typed_array.length;
switch (type) {
case JSC::JSType::Int32ArrayType: {
JSC::JSInt32Array* array = JSC::JSInt32Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeInt32>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(int32_t));
}

return array;
}
case JSC::JSType::Uint32ArrayType: {
JSC::JSUint32Array* array = JSC::JSUint32Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeUint32>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(uint32_t));
}
return array;
}
case JSC::JSType::Int16ArrayType: {
JSC::JSInt16Array* array = JSC::JSInt16Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeInt16>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(int16_t));
}

return array;
}
case JSC::JSType::Uint16ArrayType: {
JSC::JSUint16Array* array = JSC::JSUint16Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeUint16>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(uint16_t));
}
return array;
}
case JSC::JSType::Float16ArrayType: {
JSC::JSFloat16Array* array = JSC::JSFloat16Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeFloat16>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * 2); // sizeof(float16_t)
}
return array;
}
case JSC::JSType::Float32ArrayType: {
JSC::JSFloat32Array* array = JSC::JSFloat32Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeFloat32>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(float));
case DataCellTag::PackedArray: {
const uint8_t* data = cell.value.packed_array.data;
uint32_t length = cell.value.packed_array.length;
bool isFloat = cell.value.packed_array.elem == PackedArrayElem::Float4;

auto readBits = [data](uint32_t i) -> uint32_t {
uint32_t bigEndian;
memcpy(&bigEndian, data + static_cast<size_t>(i) * 8 + 4, sizeof(bigEndian));
return __builtin_bswap32(bigEndian);
};
auto readFloat = [&](uint32_t i) -> double {
return static_cast<double>(std::bit_cast<float>(readBits(i)));
};

// A double array cannot hold NaN (the hole value).
IndexingType indexingType = ArrayWithInt32;
if (isFloat) {
indexingType = ArrayWithDouble;
for (uint32_t i = 0; i < length; i++) {
if (std::isnan(readFloat(i))) {
indexingType = ArrayWithContiguous;
break;
}
}
return array;
}
case JSC::JSType::Float64ArrayType: {
JSC::JSFloat64Array* array = JSC::JSFloat64Array::createUninitialized(globalObject, globalObject->typedArrayStructureWithTypedArrayType<TypedArrayType::TypeFloat64>(), length);
RETURN_IF_EXCEPTION(scope, {});

if (length > 0) {
memcpy(array->vector(), reinterpret_cast<void*>(cell.value.typed_array.data), length * sizeof(double));
JSC::JSArray* array = nullptr;
{
JSC::ObjectInitializationScope initializationScope(vm);
array = JSC::JSArray::tryCreateUninitializedRestricted(initializationScope, nullptr, globalObject->arrayStructureForIndexingTypeDuringAllocation(indexingType), length);
if (array) {
switch (indexingType) {
case ArrayWithInt32:
for (uint32_t i = 0; i < length; i++)
array->initializeIndexWithoutBarrier(initializationScope, i, jsNumber(static_cast<int32_t>(readBits(i))), ArrayWithInt32);
break;
case ArrayWithDouble:
for (uint32_t i = 0; i < length; i++)
array->initializeIndexWithoutBarrier(initializationScope, i, jsDoubleNumber(readFloat(i)), ArrayWithDouble);
break;
default:
for (uint32_t i = 0; i < length; i++)
array->initializeIndexWithoutBarrier(initializationScope, i, jsNumber(readFloat(i)), ArrayWithContiguous);
break;
}
}
return array;
}
default: {
RELEASE_ASSERT_NOT_REACHED_WITH_MESSAGE("TODO: implement this typed array type");
}
if (!array) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return {};
}
return array;
}
default: {
RELEASE_ASSERT_NOT_REACHED();
Expand Down
2 changes: 0 additions & 2 deletions src/sql/postgres/AnyPostgresError.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ pub enum AnyPostgresError {
InvalidServerKey,
InvalidServerSignature,
JSError,
MultidimensionalArrayNotSupportedYet,
NullsInArrayNotSupportedYet,
OutOfMemory,
Overflow,
PBKDFD2,
Expand Down
69 changes: 1 addition & 68 deletions src/sql/postgres/types/Tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,72 +206,5 @@ impl Tag {
0
}

// `toJSTypedArrayType` / `toJS` / `fromJS` are extension-trait methods in
// `bun_sql_jsc`.

// There is deliberately no in-place overlay of a struct of i32 fields onto
// the `&[u8]` wire buffer here. That would be UB on two axes: (1) the recv
// buffer carries no 4-byte alignment guarantee, and (2) writing through a
// pointer derived from `&[u8]` violates Stacked Borrows / lets LLVM elide
// the writes via the `readonly` parameter attribute (observed:
// release-asan left `len` un-swapped → 192MB OOB memcpy in SQLClient.cpp).
// Instead, `bun_sql_jsc::postgres::DataCell::from_bytes_typed_array` does
// explicit unaligned field reads + copies into an owned buffer. See that
// function for the wire layout and the `.int4_array => i32` /
// `.float4_array => f32` element-type mapping.
}

// Documentation of the binary single-dimension-array wire header shape:
//
// struct array_int4 {
// int4_t ndim; /* Number of dimensions */
// int4_t _ign; /* offset for data, removed by libpq */
// Oid elemtype; /* type of element in the array */
// /* First dimension */
// int4_t size; /* Number of elements */
// int4_t index; /* Index of first element */
// int4_t first_value; /* Beginning of integer data */
// };

/// Wire-order byte swap for the binary-array element types (`i32` / `f32`).
/// Used by `bun_sql_jsc::postgres::DataCell::from_bytes_typed_array`. Uses safe
/// `to_bits`/`from_bits` instead of a `transmute_copy` shim, and safe
/// `from_ne_bytes`/`to_ne_bytes` slice round-trips instead of per-element
/// `ptr::{read,write}_unaligned` casts.
pub trait WireByteSwap: Copy {
fn wire_byte_swap(self) -> Self;
/// Safe replacement for `ptr::read_unaligned(bytes.as_ptr().cast::<Self>())`:
/// `bytes.len()` must equal `size_of::<Self>()` (panics otherwise).
fn from_unaligned_ne_bytes(bytes: &[u8]) -> Self;
/// Safe replacement for `ptr::write_unaligned(out.as_mut_ptr().cast::<Self>(), self)`:
/// `out.len()` must equal `size_of::<Self>()` (panics otherwise).
fn write_unaligned_ne_bytes(self, out: &mut [u8]);
}
impl WireByteSwap for i32 {
#[inline]
fn wire_byte_swap(self) -> Self {
self.swap_bytes()
}
#[inline]
fn from_unaligned_ne_bytes(b: &[u8]) -> Self {
Self::from_ne_bytes(b.try_into().expect("size_of::<i32>"))
}
#[inline]
fn write_unaligned_ne_bytes(self, out: &mut [u8]) {
out.copy_from_slice(&self.to_ne_bytes());
}
}
impl WireByteSwap for f32 {
#[inline]
fn wire_byte_swap(self) -> Self {
f32::from_bits(self.to_bits().swap_bytes())
}
#[inline]
fn from_unaligned_ne_bytes(b: &[u8]) -> Self {
Self::from_ne_bytes(b.try_into().expect("size_of::<f32>"))
}
#[inline]
fn write_unaligned_ne_bytes(self, out: &mut [u8]) {
out.copy_from_slice(&self.to_ne_bytes());
}
// `toJS` / `fromJS` are extension-trait methods in `bun_sql_jsc`.
}
3 changes: 0 additions & 3 deletions src/sql_jsc/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ pub enum Error {
InvalidServerKey,
#[error("InvalidServerSignature")]
InvalidServerSignature,
#[error("UnsupportedArrayType")]
UnsupportedArrayType,
#[error("JSError")]
JSError,
#[error("InvalidBinaryValue")]
Expand Down Expand Up @@ -49,7 +47,6 @@ impl Error {
Self::PBKDFD2 => "PBKDFD2",
Self::InvalidServerKey => "InvalidServerKey",
Self::InvalidServerSignature => "InvalidServerSignature",
Self::UnsupportedArrayType => "UnsupportedArrayType",
Self::JSError => "JSError",
Self::InvalidBinaryValue => "InvalidBinaryValue",
Self::Thrown => "Thrown",
Expand Down
Loading
Loading