mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 22:32:06 +00:00
1104 lines
50 KiB
Zig
1104 lines
50 KiB
Zig
pub const DataCell = extern struct {
|
||
tag: Tag,
|
||
|
||
value: Value,
|
||
free_value: u8 = 0,
|
||
isIndexedColumn: u8 = 0,
|
||
index: u32 = 0,
|
||
|
||
pub const Tag = enum(u8) {
|
||
null = 0,
|
||
string = 1,
|
||
float8 = 2,
|
||
int4 = 3,
|
||
int8 = 4,
|
||
bool = 5,
|
||
date = 6,
|
||
date_with_time_zone = 7,
|
||
bytea = 8,
|
||
json = 9,
|
||
array = 10,
|
||
typed_array = 11,
|
||
raw = 12,
|
||
uint4 = 13,
|
||
};
|
||
|
||
pub const Value = extern union {
|
||
null: u8,
|
||
string: ?bun.WTF.StringImpl,
|
||
float8: f64,
|
||
int4: i32,
|
||
int8: i64,
|
||
bool: u8,
|
||
date: f64,
|
||
date_with_time_zone: f64,
|
||
bytea: [2]usize,
|
||
json: ?bun.WTF.StringImpl,
|
||
array: Array,
|
||
typed_array: TypedArray,
|
||
raw: Raw,
|
||
uint4: u32,
|
||
};
|
||
|
||
pub const Array = extern struct {
|
||
ptr: ?[*]DataCell = null,
|
||
len: u32,
|
||
cap: u32,
|
||
pub fn slice(this: *Array) []DataCell {
|
||
const ptr = this.ptr orelse return &.{};
|
||
return ptr[0..this.len];
|
||
}
|
||
|
||
pub fn allocatedSlice(this: *Array) []DataCell {
|
||
const ptr = this.ptr orelse return &.{};
|
||
return ptr[0..this.cap];
|
||
}
|
||
|
||
pub fn deinit(this: *Array) void {
|
||
const allocated = this.allocatedSlice();
|
||
this.ptr = null;
|
||
this.len = 0;
|
||
this.cap = 0;
|
||
bun.default_allocator.free(allocated);
|
||
}
|
||
};
|
||
pub const Raw = extern struct {
|
||
ptr: ?[*]const u8 = null,
|
||
len: u64,
|
||
};
|
||
pub const TypedArray = extern struct {
|
||
head_ptr: ?[*]u8 = null,
|
||
ptr: ?[*]u8 = null,
|
||
len: u32,
|
||
byte_len: u32,
|
||
type: JSValue.JSType,
|
||
|
||
pub fn slice(this: *TypedArray) []u8 {
|
||
const ptr = this.ptr orelse return &.{};
|
||
return ptr[0..this.len];
|
||
}
|
||
|
||
pub fn byteSlice(this: *TypedArray) []u8 {
|
||
const ptr = this.head_ptr orelse return &.{};
|
||
return ptr[0..this.len];
|
||
}
|
||
};
|
||
|
||
pub fn deinit(this: *DataCell) void {
|
||
if (this.free_value == 0) return;
|
||
|
||
switch (this.tag) {
|
||
.string => {
|
||
if (this.value.string) |str| {
|
||
str.deref();
|
||
}
|
||
},
|
||
.json => {
|
||
if (this.value.json) |str| {
|
||
str.deref();
|
||
}
|
||
},
|
||
.bytea => {
|
||
if (this.value.bytea[1] == 0) return;
|
||
const slice = @as([*]u8, @ptrFromInt(this.value.bytea[0]))[0..this.value.bytea[1]];
|
||
bun.default_allocator.free(slice);
|
||
},
|
||
.array => {
|
||
for (this.value.array.slice()) |*cell| {
|
||
cell.deinit();
|
||
}
|
||
this.value.array.deinit();
|
||
},
|
||
.typed_array => {
|
||
bun.default_allocator.free(this.value.typed_array.byteSlice());
|
||
},
|
||
|
||
else => {},
|
||
}
|
||
}
|
||
pub fn raw(optional_bytes: ?*Data) DataCell {
|
||
if (optional_bytes) |bytes| {
|
||
const bytes_slice = bytes.slice();
|
||
return DataCell{
|
||
.tag = .raw,
|
||
.value = .{ .raw = .{ .ptr = @ptrCast(bytes_slice.ptr), .len = bytes_slice.len } },
|
||
};
|
||
}
|
||
// TODO: check empty and null fields
|
||
return DataCell{
|
||
.tag = .null,
|
||
.value = .{ .null = 0 },
|
||
};
|
||
}
|
||
|
||
fn parseBytea(hex: []const u8) !DataCell {
|
||
const len = hex.len / 2;
|
||
const buf = try bun.default_allocator.alloc(u8, len);
|
||
errdefer bun.default_allocator.free(buf);
|
||
|
||
return DataCell{
|
||
.tag = .bytea,
|
||
.value = .{
|
||
.bytea = .{
|
||
@intFromPtr(buf.ptr),
|
||
try bun.strings.decodeHexToBytes(buf, u8, hex),
|
||
},
|
||
},
|
||
.free_value = 1,
|
||
};
|
||
}
|
||
|
||
fn unescapePostgresString(input: []const u8, buffer: []u8) ![]u8 {
|
||
var out_index: usize = 0;
|
||
var i: usize = 0;
|
||
|
||
while (i < input.len) : (i += 1) {
|
||
if (out_index >= buffer.len) return error.BufferTooSmall;
|
||
|
||
if (input[i] == '\\' and i + 1 < input.len) {
|
||
i += 1;
|
||
switch (input[i]) {
|
||
// Common escapes
|
||
'b' => buffer[out_index] = '\x08', // Backspace
|
||
'f' => buffer[out_index] = '\x0C', // Form feed
|
||
'n' => buffer[out_index] = '\n', // Line feed
|
||
'r' => buffer[out_index] = '\r', // Carriage return
|
||
't' => buffer[out_index] = '\t', // Tab
|
||
'"' => buffer[out_index] = '"', // Double quote
|
||
'\\' => buffer[out_index] = '\\', // Backslash
|
||
'\'' => buffer[out_index] = '\'', // Single quote
|
||
|
||
// JSON allows forward slash escaping
|
||
'/' => buffer[out_index] = '/',
|
||
|
||
// PostgreSQL hex escapes (used for unicode too)
|
||
'x' => {
|
||
if (i + 2 >= input.len) return error.InvalidEscapeSequence;
|
||
const hex_value = try std.fmt.parseInt(u8, input[i + 1 .. i + 3], 16);
|
||
buffer[out_index] = hex_value;
|
||
i += 2;
|
||
},
|
||
|
||
else => return error.UnknownEscapeSequence,
|
||
}
|
||
} else {
|
||
buffer[out_index] = input[i];
|
||
}
|
||
out_index += 1;
|
||
}
|
||
|
||
return buffer[0..out_index];
|
||
}
|
||
fn trySlice(slice: []const u8, count: usize) []const u8 {
|
||
if (slice.len <= count) return "";
|
||
return slice[count..];
|
||
}
|
||
fn parseArray(bytes: []const u8, bigint: bool, comptime arrayType: types.Tag, globalObject: *JSC.JSGlobalObject, offset: ?*usize, comptime is_json_sub_array: bool) !DataCell {
|
||
const closing_brace = if (is_json_sub_array) ']' else '}';
|
||
const opening_brace = if (is_json_sub_array) '[' else '{';
|
||
if (bytes.len < 2 or bytes[0] != opening_brace) {
|
||
return error.UnsupportedArrayFormat;
|
||
}
|
||
// empty array
|
||
if (bytes.len == 2 and bytes[1] == closing_brace) {
|
||
if (offset) |offset_ptr| {
|
||
offset_ptr.* = 2;
|
||
}
|
||
return DataCell{ .tag = .array, .value = .{ .array = .{ .ptr = null, .len = 0, .cap = 0 } } };
|
||
}
|
||
|
||
var array = std.ArrayListUnmanaged(DataCell){};
|
||
var stack_buffer: [16 * 1024]u8 = undefined;
|
||
|
||
errdefer {
|
||
if (array.capacity > 0) array.deinit(bun.default_allocator);
|
||
}
|
||
var slice = bytes[1..];
|
||
var reached_end = false;
|
||
const separator = switch (arrayType) {
|
||
.box_array => ';',
|
||
else => ',',
|
||
};
|
||
while (slice.len > 0) {
|
||
switch (slice[0]) {
|
||
closing_brace => {
|
||
if (reached_end) {
|
||
// cannot reach end twice
|
||
return error.UnsupportedArrayFormat;
|
||
}
|
||
// end of array
|
||
reached_end = true;
|
||
slice = trySlice(slice, 1);
|
||
break;
|
||
},
|
||
opening_brace => {
|
||
var sub_array_offset: usize = 0;
|
||
const sub_array = try parseArray(slice, bigint, arrayType, globalObject, &sub_array_offset, is_json_sub_array);
|
||
try array.append(bun.default_allocator, sub_array);
|
||
slice = trySlice(slice, sub_array_offset);
|
||
continue;
|
||
},
|
||
'"' => {
|
||
// parse string
|
||
var current_idx: usize = 0;
|
||
const source = slice[1..];
|
||
// simple escape check to avoid something like "\\\\" and "\""
|
||
var is_escaped = false;
|
||
for (source, 0..source.len) |byte, index| {
|
||
if (byte == '"' and !is_escaped) {
|
||
current_idx = index + 1;
|
||
break;
|
||
}
|
||
is_escaped = !is_escaped and byte == '\\';
|
||
}
|
||
// did not find a closing quote
|
||
if (current_idx == 0) return error.UnsupportedArrayFormat;
|
||
switch (arrayType) {
|
||
.bytea_array => {
|
||
// this is a bytea array so we need to parse the bytea strings
|
||
const bytea_bytes = slice[1..current_idx];
|
||
if (bun.strings.startsWith(bytea_bytes, "\\\\x")) {
|
||
// its a bytea string lets parse it as a bytea
|
||
try array.append(bun.default_allocator, try parseBytea(bytea_bytes[3..][0 .. bytea_bytes.len - 3]));
|
||
slice = trySlice(slice, current_idx + 1);
|
||
continue;
|
||
}
|
||
// invalid bytea array
|
||
return error.UnsupportedByteaFormat;
|
||
},
|
||
.timestamptz_array,
|
||
.timestamp_array,
|
||
.date_array,
|
||
=> {
|
||
const date_str = slice[1..current_idx];
|
||
var str = bun.String.init(date_str);
|
||
defer str.deref();
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .date, .value = .{ .date = str.parseDate(globalObject) } });
|
||
|
||
slice = trySlice(slice, current_idx + 1);
|
||
continue;
|
||
},
|
||
.json_array,
|
||
.jsonb_array,
|
||
=> {
|
||
const str_bytes = slice[1..current_idx];
|
||
const needs_dynamic_buffer = str_bytes.len < stack_buffer.len;
|
||
const buffer = if (needs_dynamic_buffer) try bun.default_allocator.alloc(u8, str_bytes.len) else stack_buffer[0..];
|
||
defer if (needs_dynamic_buffer) bun.default_allocator.free(buffer);
|
||
const unescaped = unescapePostgresString(str_bytes, buffer) catch return error.InvalidByteSequence;
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .json, .value = .{ .json = if (unescaped.len > 0) String.createUTF8(unescaped).value.WTFStringImpl else null }, .free_value = 1 });
|
||
slice = trySlice(slice, current_idx + 1);
|
||
continue;
|
||
},
|
||
else => {},
|
||
}
|
||
const str_bytes = slice[1..current_idx];
|
||
if (str_bytes.len == 0) {
|
||
// empty string
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .string, .value = .{ .string = null }, .free_value = 1 });
|
||
slice = trySlice(slice, current_idx + 1);
|
||
continue;
|
||
}
|
||
const needs_dynamic_buffer = str_bytes.len < stack_buffer.len;
|
||
const buffer = if (needs_dynamic_buffer) try bun.default_allocator.alloc(u8, str_bytes.len) else stack_buffer[0..];
|
||
defer if (needs_dynamic_buffer) bun.default_allocator.free(buffer);
|
||
const string_bytes = unescapePostgresString(str_bytes, buffer) catch return error.InvalidByteSequence;
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .string, .value = .{ .string = if (string_bytes.len > 0) String.createUTF8(string_bytes).value.WTFStringImpl else null }, .free_value = 1 });
|
||
|
||
slice = trySlice(slice, current_idx + 1);
|
||
continue;
|
||
},
|
||
separator => {
|
||
// next element or positive number, just advance
|
||
slice = trySlice(slice, 1);
|
||
continue;
|
||
},
|
||
else => {
|
||
switch (arrayType) {
|
||
// timez, date, time, interval are handled like single string cases
|
||
.timetz_array,
|
||
.date_array,
|
||
.time_array,
|
||
.interval_array,
|
||
// text array types
|
||
.bpchar_array,
|
||
.varchar_array,
|
||
.char_array,
|
||
.text_array,
|
||
.name_array,
|
||
.numeric_array,
|
||
.money_array,
|
||
.varbit_array,
|
||
.int2vector_array,
|
||
.bit_array,
|
||
.path_array,
|
||
.xml_array,
|
||
.point_array,
|
||
.lseg_array,
|
||
.box_array,
|
||
.polygon_array,
|
||
.line_array,
|
||
.cidr_array,
|
||
.circle_array,
|
||
.macaddr8_array,
|
||
.macaddr_array,
|
||
.inet_array,
|
||
.aclitem_array,
|
||
.pg_database_array,
|
||
.pg_database_array2,
|
||
=> {
|
||
// this is also a string until we reach "," or "}" but a single word string like Bun
|
||
var current_idx: usize = 0;
|
||
|
||
for (slice, 0..slice.len) |byte, index| {
|
||
switch (byte) {
|
||
'}', separator => {
|
||
current_idx = index;
|
||
break;
|
||
},
|
||
else => {},
|
||
}
|
||
}
|
||
if (current_idx == 0) return error.UnsupportedArrayFormat;
|
||
const 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 (bun.strings.eqlComptime(element, "NULL")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .null, .value = .{ .null = 0 } });
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
}
|
||
if (arrayType == .date_array) {
|
||
var str = bun.String.init(element);
|
||
defer str.deref();
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .date, .value = .{ .date = str.parseDate(globalObject) } });
|
||
} else {
|
||
// the only escape sequency possible here is \b
|
||
if (bun.strings.eqlComptime(element, "\\b")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .string, .value = .{ .string = bun.String.createUTF8("\x08").value.WTFStringImpl }, .free_value = 1 });
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .string, .value = .{ .string = if (element.len > 0) bun.String.createUTF8(element).value.WTFStringImpl else null }, .free_value = 0 });
|
||
}
|
||
}
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
},
|
||
else => {
|
||
// non text array, NaN, Null, False, True etc are special cases here
|
||
switch (slice[0]) {
|
||
'N' => {
|
||
// null or nan
|
||
if (slice.len < 3) return error.UnsupportedArrayFormat;
|
||
if (slice.len >= 4) {
|
||
if (bun.strings.eqlComptime(slice[0..4], "NULL")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .null, .value = .{ .null = 0 } });
|
||
slice = trySlice(slice, 4);
|
||
continue;
|
||
}
|
||
}
|
||
if (bun.strings.eqlComptime(slice[0..3], "NaN")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .float8, .value = .{ .float8 = std.math.nan(f64) } });
|
||
slice = trySlice(slice, 3);
|
||
continue;
|
||
}
|
||
return error.UnsupportedArrayFormat;
|
||
},
|
||
'f' => {
|
||
// false
|
||
if (arrayType == .json_array or arrayType == .jsonb_array) {
|
||
if (slice.len < 5) return error.UnsupportedArrayFormat;
|
||
if (bun.strings.eqlComptime(slice[0..5], "false")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .bool, .value = .{ .bool = 0 } });
|
||
slice = trySlice(slice, 5);
|
||
continue;
|
||
}
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .bool, .value = .{ .bool = 0 } });
|
||
slice = trySlice(slice, 1);
|
||
continue;
|
||
}
|
||
},
|
||
't' => {
|
||
// true
|
||
if (arrayType == .json_array or arrayType == .jsonb_array) {
|
||
if (slice.len < 4) return error.UnsupportedArrayFormat;
|
||
if (bun.strings.eqlComptime(slice[0..4], "true")) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .bool, .value = .{ .bool = 1 } });
|
||
slice = trySlice(slice, 4);
|
||
continue;
|
||
}
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .bool, .value = .{ .bool = 1 } });
|
||
slice = trySlice(slice, 1);
|
||
continue;
|
||
}
|
||
},
|
||
'I',
|
||
'i',
|
||
=> {
|
||
// infinity
|
||
if (slice.len < 8) return error.UnsupportedArrayFormat;
|
||
|
||
if (bun.strings.eqlCaseInsensitiveASCII(slice[0..8], "Infinity", false)) {
|
||
if (arrayType == .date_array or arrayType == .timestamp_array or arrayType == .timestamptz_array) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .date, .value = .{ .date = std.math.inf(f64) } });
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .float8, .value = .{ .float8 = std.math.inf(f64) } });
|
||
}
|
||
slice = trySlice(slice, 8);
|
||
continue;
|
||
}
|
||
|
||
return error.UnsupportedArrayFormat;
|
||
},
|
||
'+' => {
|
||
slice = trySlice(slice, 1);
|
||
continue;
|
||
},
|
||
'-', '0'...'9' => {
|
||
// parse number, detect float, int, if starts with - it can be -Infinity or -Infinity
|
||
var is_negative = false;
|
||
var is_float = false;
|
||
var current_idx: usize = 0;
|
||
var is_infinity = false;
|
||
// track exponent stuff (1.1e-12, 1.1e+12)
|
||
var has_exponent = false;
|
||
var has_negative_sign = false;
|
||
var has_positive_sign = false;
|
||
for (slice, 0..slice.len) |byte, index| {
|
||
switch (byte) {
|
||
'0'...'9' => {},
|
||
closing_brace, separator => {
|
||
current_idx = index;
|
||
// end of element
|
||
break;
|
||
},
|
||
'e' => {
|
||
if (!is_float) return error.UnsupportedArrayFormat;
|
||
if (has_exponent) return error.UnsupportedArrayFormat;
|
||
has_exponent = true;
|
||
continue;
|
||
},
|
||
'+' => {
|
||
if (!has_exponent) return error.UnsupportedArrayFormat;
|
||
if (has_positive_sign) return error.UnsupportedArrayFormat;
|
||
has_positive_sign = true;
|
||
continue;
|
||
},
|
||
'-' => {
|
||
if (index == 0) {
|
||
is_negative = true;
|
||
continue;
|
||
}
|
||
if (!has_exponent) return error.UnsupportedArrayFormat;
|
||
if (has_negative_sign) return error.UnsupportedArrayFormat;
|
||
has_negative_sign = true;
|
||
continue;
|
||
},
|
||
'.' => {
|
||
// we can only have one dot and the dot must be before the exponent
|
||
if (is_float) return error.UnsupportedArrayFormat;
|
||
is_float = true;
|
||
},
|
||
'I', 'i' => {
|
||
// infinity
|
||
is_infinity = true;
|
||
const element = if (is_negative) slice[1..] else slice;
|
||
if (element.len < 8) return error.UnsupportedArrayFormat;
|
||
if (bun.strings.eqlCaseInsensitiveASCII(element[0..8], "Infinity", false)) {
|
||
if (arrayType == .date_array or arrayType == .timestamp_array or arrayType == .timestamptz_array) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .date, .value = .{ .date = if (is_negative) -std.math.inf(f64) else std.math.inf(f64) } });
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .float8, .value = .{ .float8 = if (is_negative) -std.math.inf(f64) else std.math.inf(f64) } });
|
||
}
|
||
slice = trySlice(slice, 8 + @as(usize, @intFromBool(is_negative)));
|
||
break;
|
||
}
|
||
|
||
return error.UnsupportedArrayFormat;
|
||
},
|
||
else => {
|
||
return error.UnsupportedArrayFormat;
|
||
},
|
||
}
|
||
}
|
||
if (is_infinity) {
|
||
continue;
|
||
}
|
||
if (current_idx == 0) return error.UnsupportedArrayFormat;
|
||
const element = slice[0..current_idx];
|
||
if (is_float or arrayType == .float8_array) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .float8, .value = .{ .float8 = bun.parseDouble(element) catch std.math.nan(f64) } });
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
}
|
||
switch (arrayType) {
|
||
.int8_array => {
|
||
if (bigint) {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .int8, .value = .{ .int8 = std.fmt.parseInt(i64, element, 0) catch return error.UnsupportedArrayFormat } });
|
||
} else {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .string, .value = .{ .string = if (element.len > 0) bun.String.createUTF8(element).value.WTFStringImpl else null }, .free_value = 1 });
|
||
}
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
},
|
||
.cid_array, .xid_array, .oid_array => {
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .uint4, .value = .{ .uint4 = std.fmt.parseInt(u32, element, 0) catch 0 } });
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
},
|
||
else => {
|
||
const value = std.fmt.parseInt(i32, element, 0) catch return error.UnsupportedArrayFormat;
|
||
|
||
try array.append(bun.default_allocator, DataCell{ .tag = .int4, .value = .{ .int4 = @intCast(value) } });
|
||
slice = trySlice(slice, current_idx);
|
||
continue;
|
||
},
|
||
}
|
||
},
|
||
else => {
|
||
if (arrayType == .json_array or arrayType == .jsonb_array) {
|
||
if (slice[0] == '[') {
|
||
var sub_array_offset: usize = 0;
|
||
const sub_array = try parseArray(slice, bigint, arrayType, globalObject, &sub_array_offset, true);
|
||
try array.append(bun.default_allocator, sub_array);
|
||
slice = trySlice(slice, sub_array_offset);
|
||
continue;
|
||
}
|
||
}
|
||
return error.UnsupportedArrayFormat;
|
||
},
|
||
}
|
||
},
|
||
}
|
||
},
|
||
}
|
||
}
|
||
|
||
if (offset) |offset_ptr| {
|
||
offset_ptr.* = bytes.len - slice.len;
|
||
}
|
||
|
||
// postgres dont really support arrays with more than 2^31 elements, 2ˆ32 is the max we support, but users should never reach this branch
|
||
if (!reached_end or array.items.len > std.math.maxInt(u32)) {
|
||
@branchHint(.unlikely);
|
||
|
||
return error.UnsupportedArrayFormat;
|
||
}
|
||
return DataCell{ .tag = .array, .value = .{ .array = .{ .ptr = array.items.ptr, .len = @truncate(array.items.len), .cap = @truncate(array.capacity) } } };
|
||
}
|
||
|
||
pub fn fromBytes(binary: bool, bigint: bool, oid: types.Tag, bytes: []const u8, globalObject: *JSC.JSGlobalObject) !DataCell {
|
||
switch (oid) {
|
||
// TODO: .int2_array, .float8_array
|
||
inline .int4_array, .float4_array => |tag| {
|
||
if (binary) {
|
||
if (bytes.len < 16) {
|
||
return error.InvalidBinaryData;
|
||
}
|
||
// https://github.com/postgres/postgres/blob/master/src/backend/utils/adt/arrayfuncs.c#L1549-L1645
|
||
const dimensions_raw: int4 = @bitCast(bytes[0..4].*);
|
||
const contains_nulls: int4 = @bitCast(bytes[4..8].*);
|
||
|
||
const dimensions = @byteSwap(dimensions_raw);
|
||
if (dimensions > 1) {
|
||
return error.MultidimensionalArrayNotSupportedYet;
|
||
}
|
||
|
||
if (contains_nulls != 0) {
|
||
return error.NullsInArrayNotSupportedYet;
|
||
}
|
||
|
||
if (dimensions == 0) {
|
||
return DataCell{
|
||
.tag = .typed_array,
|
||
.value = .{
|
||
.typed_array = .{
|
||
.ptr = null,
|
||
.len = 0,
|
||
.byte_len = 0,
|
||
.type = try tag.toJSTypedArrayType(),
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
const elements = (try tag.pgArrayType()).init(bytes).slice();
|
||
|
||
return DataCell{
|
||
.tag = .typed_array,
|
||
.value = .{
|
||
.typed_array = .{
|
||
.head_ptr = if (bytes.len > 0) @constCast(bytes.ptr) else null,
|
||
.ptr = if (elements.len > 0) @ptrCast(elements.ptr) else null,
|
||
.len = @truncate(elements.len),
|
||
.byte_len = @truncate(bytes.len),
|
||
.type = try tag.toJSTypedArrayType(),
|
||
},
|
||
},
|
||
};
|
||
} else {
|
||
return try parseArray(bytes, bigint, tag, globalObject, null, false);
|
||
}
|
||
},
|
||
.int2 => {
|
||
if (binary) {
|
||
return DataCell{ .tag = .int4, .value = .{ .int4 = try parseBinary(.int2, i16, bytes) } };
|
||
} else {
|
||
return DataCell{ .tag = .int4, .value = .{ .int4 = std.fmt.parseInt(i32, bytes, 0) catch 0 } };
|
||
}
|
||
},
|
||
.cid, .xid, .oid => {
|
||
if (binary) {
|
||
return DataCell{ .tag = .uint4, .value = .{ .uint4 = try parseBinary(.oid, u32, bytes) } };
|
||
} else {
|
||
return DataCell{ .tag = .uint4, .value = .{ .uint4 = std.fmt.parseInt(u32, bytes, 0) catch 0 } };
|
||
}
|
||
},
|
||
.int4 => {
|
||
if (binary) {
|
||
return DataCell{ .tag = .int4, .value = .{ .int4 = try parseBinary(.int4, i32, bytes) } };
|
||
} else {
|
||
return DataCell{ .tag = .int4, .value = .{ .int4 = std.fmt.parseInt(i32, bytes, 0) catch 0 } };
|
||
}
|
||
},
|
||
// postgres when reading bigint as int8 it returns a string unless type: { bigint: postgres.BigInt is set
|
||
.int8 => {
|
||
if (bigint) {
|
||
// .int8 is a 64-bit integer always string
|
||
return DataCell{ .tag = .int8, .value = .{ .int8 = std.fmt.parseInt(i64, bytes, 0) catch 0 } };
|
||
} else {
|
||
return DataCell{ .tag = .string, .value = .{ .string = if (bytes.len > 0) bun.String.createUTF8(bytes).value.WTFStringImpl else null }, .free_value = 1 };
|
||
}
|
||
},
|
||
.float8 => {
|
||
if (binary and bytes.len == 8) {
|
||
return DataCell{ .tag = .float8, .value = .{ .float8 = try parseBinary(.float8, f64, bytes) } };
|
||
} else {
|
||
const float8: f64 = bun.parseDouble(bytes) catch std.math.nan(f64);
|
||
return DataCell{ .tag = .float8, .value = .{ .float8 = float8 } };
|
||
}
|
||
},
|
||
.float4 => {
|
||
if (binary and bytes.len == 4) {
|
||
return DataCell{ .tag = .float8, .value = .{ .float8 = try parseBinary(.float4, f32, bytes) } };
|
||
} else {
|
||
const float4: f64 = bun.parseDouble(bytes) catch std.math.nan(f64);
|
||
return DataCell{ .tag = .float8, .value = .{ .float8 = float4 } };
|
||
}
|
||
},
|
||
.numeric => {
|
||
if (binary) {
|
||
// this is probrably good enough for most cases
|
||
var stack_buffer = std.heap.stackFallback(1024, bun.default_allocator);
|
||
const allocator = stack_buffer.get();
|
||
var numeric_buffer = std.ArrayList(u8).fromOwnedSlice(allocator, &stack_buffer.buffer);
|
||
numeric_buffer.items.len = 0;
|
||
defer numeric_buffer.deinit();
|
||
|
||
// if is binary format lets display as a string because JS cant handle it in a safe way
|
||
const result = parseBinaryNumeric(bytes, &numeric_buffer) catch return error.UnsupportedNumericFormat;
|
||
return DataCell{ .tag = .string, .value = .{ .string = bun.String.createUTF8(result.slice()).value.WTFStringImpl }, .free_value = 1 };
|
||
} else {
|
||
// nice text is actually what we want here
|
||
return DataCell{ .tag = .string, .value = .{ .string = if (bytes.len > 0) String.createUTF8(bytes).value.WTFStringImpl else null }, .free_value = 1 };
|
||
}
|
||
},
|
||
.jsonb, .json => {
|
||
return DataCell{ .tag = .json, .value = .{ .json = if (bytes.len > 0) String.createUTF8(bytes).value.WTFStringImpl else null }, .free_value = 1 };
|
||
},
|
||
.bool => {
|
||
if (binary) {
|
||
return DataCell{ .tag = .bool, .value = .{ .bool = @intFromBool(bytes.len > 0 and bytes[0] == 1) } };
|
||
} else {
|
||
return DataCell{ .tag = .bool, .value = .{ .bool = @intFromBool(bytes.len > 0 and bytes[0] == 't') } };
|
||
}
|
||
},
|
||
.date, .timestamp, .timestamptz => |tag| {
|
||
if (binary and bytes.len == 8) {
|
||
switch (tag) {
|
||
.timestamptz => return DataCell{ .tag = .date_with_time_zone, .value = .{ .date_with_time_zone = types.date.fromBinary(bytes) } },
|
||
.timestamp => return DataCell{ .tag = .date, .value = .{ .date = types.date.fromBinary(bytes) } },
|
||
else => unreachable,
|
||
}
|
||
} else {
|
||
var str = bun.String.init(bytes);
|
||
defer str.deref();
|
||
return DataCell{ .tag = .date, .value = .{ .date = str.parseDate(globalObject) } };
|
||
}
|
||
},
|
||
|
||
.bytea => {
|
||
if (binary) {
|
||
return DataCell{ .tag = .bytea, .value = .{ .bytea = .{ @intFromPtr(bytes.ptr), bytes.len } } };
|
||
} else {
|
||
if (bun.strings.hasPrefixComptime(bytes, "\\x")) {
|
||
return try parseBytea(bytes[2..]);
|
||
}
|
||
return error.UnsupportedByteaFormat;
|
||
}
|
||
},
|
||
// text array types
|
||
inline .bpchar_array,
|
||
.varchar_array,
|
||
.char_array,
|
||
.text_array,
|
||
.name_array,
|
||
.json_array,
|
||
.jsonb_array,
|
||
// special types handled as text array
|
||
.path_array,
|
||
.xml_array,
|
||
.point_array,
|
||
.lseg_array,
|
||
.box_array,
|
||
.polygon_array,
|
||
.line_array,
|
||
.cidr_array,
|
||
.numeric_array,
|
||
.money_array,
|
||
.varbit_array,
|
||
.bit_array,
|
||
.int2vector_array,
|
||
.circle_array,
|
||
.macaddr8_array,
|
||
.macaddr_array,
|
||
.inet_array,
|
||
.aclitem_array,
|
||
.tid_array,
|
||
.pg_database_array,
|
||
.pg_database_array2,
|
||
// numeric array types
|
||
.int8_array,
|
||
.int2_array,
|
||
.float8_array,
|
||
.oid_array,
|
||
.xid_array,
|
||
.cid_array,
|
||
|
||
// special types
|
||
.bool_array,
|
||
.bytea_array,
|
||
|
||
//time types
|
||
.time_array,
|
||
.date_array,
|
||
.timetz_array,
|
||
.timestamp_array,
|
||
.timestamptz_array,
|
||
.interval_array,
|
||
=> |tag| {
|
||
return try parseArray(bytes, bigint, tag, globalObject, null, false);
|
||
},
|
||
else => {
|
||
return DataCell{ .tag = .string, .value = .{ .string = if (bytes.len > 0) bun.String.createUTF8(bytes).value.WTFStringImpl else null }, .free_value = 1 };
|
||
},
|
||
}
|
||
}
|
||
|
||
// #define pg_hton16(x) (x)
|
||
// #define pg_hton32(x) (x)
|
||
// #define pg_hton64(x) (x)
|
||
|
||
// #define pg_ntoh16(x) (x)
|
||
// #define pg_ntoh32(x) (x)
|
||
// #define pg_ntoh64(x) (x)
|
||
|
||
fn pg_ntoT(comptime IntSize: usize, i: anytype) std.meta.Int(.unsigned, IntSize) {
|
||
@setRuntimeSafety(false);
|
||
const T = @TypeOf(i);
|
||
if (@typeInfo(T) == .array) {
|
||
return pg_ntoT(IntSize, @as(std.meta.Int(.unsigned, IntSize), @bitCast(i)));
|
||
}
|
||
|
||
const casted: std.meta.Int(.unsigned, IntSize) = @intCast(i);
|
||
return @byteSwap(casted);
|
||
}
|
||
fn pg_ntoh16(x: anytype) u16 {
|
||
return pg_ntoT(16, x);
|
||
}
|
||
|
||
fn pg_ntoh32(x: anytype) u32 {
|
||
return pg_ntoT(32, x);
|
||
}
|
||
const PGNummericString = union(enum) {
|
||
static: [:0]const u8,
|
||
dynamic: []const u8,
|
||
|
||
pub fn slice(this: PGNummericString) []const u8 {
|
||
return switch (this) {
|
||
.static => |value| value,
|
||
.dynamic => |value| value,
|
||
};
|
||
}
|
||
};
|
||
|
||
fn parseBinaryNumeric(input: []const u8, result: *std.ArrayList(u8)) !PGNummericString {
|
||
// Reference: https://github.com/postgres/postgres/blob/50e6eb731d98ab6d0e625a0b87fb327b172bbebd/src/backend/utils/adt/numeric.c#L7612-L7740
|
||
if (input.len < 8) return error.InvalidBuffer;
|
||
var fixed_buffer = std.io.fixedBufferStream(input);
|
||
var reader = fixed_buffer.reader();
|
||
|
||
// Read header values using big-endian
|
||
const ndigits = try reader.readInt(i16, .big);
|
||
const weight = try reader.readInt(i16, .big);
|
||
const sign = try reader.readInt(u16, .big);
|
||
const dscale = try reader.readInt(i16, .big);
|
||
|
||
// Handle special cases
|
||
switch (sign) {
|
||
0xC000 => return PGNummericString{ .static = "NaN" },
|
||
0xD000 => return PGNummericString{ .static = "Infinity" },
|
||
0xF000 => return PGNummericString{ .static = "-Infinity" },
|
||
0x4000, 0x0000 => {},
|
||
else => return error.InvalidSign,
|
||
}
|
||
|
||
if (ndigits == 0) {
|
||
return PGNummericString{ .static = "0" };
|
||
}
|
||
|
||
// Add negative sign if needed
|
||
if (sign == 0x4000) {
|
||
try result.append('-');
|
||
}
|
||
|
||
// Calculate decimal point position
|
||
var decimal_pos: i32 = @as(i32, weight + 1) * 4;
|
||
if (decimal_pos <= 0) {
|
||
decimal_pos = 1;
|
||
}
|
||
// Output all digits before the decimal point
|
||
|
||
var scale_start: i32 = 0;
|
||
if (weight < 0) {
|
||
try result.append('0');
|
||
scale_start = @as(i32, @intCast(weight)) + 1;
|
||
} else {
|
||
var idx: usize = 0;
|
||
var first_non_zero = false;
|
||
|
||
while (idx <= weight) : (idx += 1) {
|
||
const digit = if (idx < ndigits) try reader.readInt(u16, .big) else 0;
|
||
var digit_str: [4]u8 = undefined;
|
||
const digit_len = std.fmt.formatIntBuf(&digit_str, digit, 10, .lower, .{ .width = 4, .fill = '0' });
|
||
if (!first_non_zero) {
|
||
//In the first digit, suppress extra leading decimal zeroes
|
||
var start_idx: usize = 0;
|
||
while (start_idx < digit_len and digit_str[start_idx] == '0') : (start_idx += 1) {}
|
||
if (start_idx == digit_len) continue;
|
||
const digit_slice = digit_str[start_idx..digit_len];
|
||
try result.appendSlice(digit_slice);
|
||
first_non_zero = true;
|
||
} else {
|
||
try result.appendSlice(digit_str[0..digit_len]);
|
||
}
|
||
}
|
||
}
|
||
// If requested, output a decimal point and all the digits that follow it.
|
||
// We initially put out a multiple of 4 digits, then truncate if needed.
|
||
if (dscale > 0) {
|
||
try result.append('.');
|
||
// negative scale means we need to add zeros before the decimal point
|
||
// greater than ndigits means we need to add zeros after the decimal point
|
||
var idx: isize = scale_start;
|
||
const end: usize = result.items.len + @as(usize, @intCast(dscale));
|
||
while (idx < dscale) : (idx += 4) {
|
||
if (idx >= 0 and idx < ndigits) {
|
||
const digit = reader.readInt(u16, .big) catch 0;
|
||
var digit_str: [4]u8 = undefined;
|
||
const digit_len = std.fmt.formatIntBuf(&digit_str, digit, 10, .lower, .{ .width = 4, .fill = '0' });
|
||
try result.appendSlice(digit_str[0..digit_len]);
|
||
} else {
|
||
try result.appendSlice("0000");
|
||
}
|
||
}
|
||
if (result.items.len > end) {
|
||
result.items.len = end;
|
||
}
|
||
}
|
||
return PGNummericString{ .dynamic = result.items };
|
||
}
|
||
|
||
pub fn parseBinary(comptime tag: types.Tag, comptime ReturnType: type, bytes: []const u8) AnyPostgresError!ReturnType {
|
||
switch (comptime tag) {
|
||
.float8 => {
|
||
return @as(f64, @bitCast(try parseBinary(.int8, i64, bytes)));
|
||
},
|
||
.int8 => {
|
||
// pq_getmsgfloat8
|
||
if (bytes.len != 8) return error.InvalidBinaryData;
|
||
return @byteSwap(@as(i64, @bitCast(bytes[0..8].*)));
|
||
},
|
||
.int4 => {
|
||
// pq_getmsgint
|
||
switch (bytes.len) {
|
||
1 => {
|
||
return bytes[0];
|
||
},
|
||
2 => {
|
||
return pg_ntoh16(@as(u16, @bitCast(bytes[0..2].*)));
|
||
},
|
||
4 => {
|
||
return @bitCast(pg_ntoh32(@as(u32, @bitCast(bytes[0..4].*))));
|
||
},
|
||
else => {
|
||
return error.UnsupportedIntegerSize;
|
||
},
|
||
}
|
||
},
|
||
.oid => {
|
||
switch (bytes.len) {
|
||
1 => {
|
||
return bytes[0];
|
||
},
|
||
2 => {
|
||
return pg_ntoh16(@as(u16, @bitCast(bytes[0..2].*)));
|
||
},
|
||
4 => {
|
||
return pg_ntoh32(@as(u32, @bitCast(bytes[0..4].*)));
|
||
},
|
||
else => {
|
||
return error.UnsupportedIntegerSize;
|
||
},
|
||
}
|
||
},
|
||
.int2 => {
|
||
// pq_getmsgint
|
||
switch (bytes.len) {
|
||
1 => {
|
||
return bytes[0];
|
||
},
|
||
2 => {
|
||
// PostgreSQL stores numbers in big-endian format, so we must read as big-endian
|
||
// Read as raw 16-bit unsigned integer
|
||
const value: u16 = @bitCast(bytes[0..2].*);
|
||
// Convert from big-endian to native-endian (we always use little endian)
|
||
return @bitCast(@byteSwap(value)); // Cast to signed 16-bit integer (i16)
|
||
},
|
||
else => {
|
||
return error.UnsupportedIntegerSize;
|
||
},
|
||
}
|
||
},
|
||
.float4 => {
|
||
// pq_getmsgfloat4
|
||
return @as(f32, @bitCast(try parseBinary(.int4, i32, bytes)));
|
||
},
|
||
else => @compileError("TODO"),
|
||
}
|
||
}
|
||
|
||
pub const Flags = packed struct(u32) {
|
||
has_indexed_columns: bool = false,
|
||
has_named_columns: bool = false,
|
||
has_duplicate_columns: bool = false,
|
||
_: u29 = 0,
|
||
};
|
||
|
||
pub const Putter = struct {
|
||
list: []DataCell,
|
||
fields: []const protocol.FieldDescription,
|
||
binary: bool = false,
|
||
bigint: bool = false,
|
||
count: usize = 0,
|
||
globalObject: *JSC.JSGlobalObject,
|
||
|
||
extern fn JSC__constructObjectFromDataCell(
|
||
*JSC.JSGlobalObject,
|
||
JSValue,
|
||
JSValue,
|
||
[*]DataCell,
|
||
u32,
|
||
Flags,
|
||
u8, // result_mode
|
||
?[*]JSC.JSObject.ExternColumnIdentifier, // names
|
||
u32, // names count
|
||
) JSValue;
|
||
|
||
pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSValue, structure: JSValue, flags: Flags, result_mode: PostgresSQLQueryResultMode, cached_structure: ?PostgresCachedStructure) JSValue {
|
||
var names: ?[*]JSC.JSObject.ExternColumnIdentifier = null;
|
||
var names_count: u32 = 0;
|
||
if (cached_structure) |c| {
|
||
if (c.fields) |f| {
|
||
names = f.ptr;
|
||
names_count = @truncate(f.len);
|
||
}
|
||
}
|
||
|
||
return JSC__constructObjectFromDataCell(
|
||
globalObject,
|
||
array,
|
||
structure,
|
||
this.list.ptr,
|
||
@truncate(this.fields.len),
|
||
flags,
|
||
@intFromEnum(result_mode),
|
||
names,
|
||
names_count,
|
||
);
|
||
}
|
||
|
||
fn putImpl(this: *Putter, index: u32, optional_bytes: ?*Data, comptime is_raw: bool) !bool {
|
||
const field = &this.fields[index];
|
||
const oid = field.type_oid;
|
||
debug("index: {d}, oid: {d}", .{ index, oid });
|
||
const cell: *DataCell = &this.list[index];
|
||
if (is_raw) {
|
||
cell.* = DataCell.raw(optional_bytes);
|
||
} else {
|
||
const tag = if (std.math.maxInt(short) < oid) .text else @as(types.Tag, @enumFromInt(@as(short, @intCast(oid))));
|
||
cell.* = if (optional_bytes) |data|
|
||
try DataCell.fromBytes((field.binary or this.binary) and tag.isBinaryFormatSupported(), this.bigint, tag, data.slice(), this.globalObject)
|
||
else
|
||
DataCell{
|
||
.tag = .null,
|
||
.value = .{
|
||
.null = 0,
|
||
},
|
||
};
|
||
}
|
||
this.count += 1;
|
||
cell.index = switch (field.name_or_index) {
|
||
// The indexed columns can be out of order.
|
||
.index => |i| i,
|
||
|
||
else => @intCast(index),
|
||
};
|
||
|
||
// TODO: when duplicate and we know the result will be an object
|
||
// and not a .values() array, we can discard the data
|
||
// immediately.
|
||
cell.isIndexedColumn = switch (field.name_or_index) {
|
||
.duplicate => 2,
|
||
.index => 1,
|
||
.name => 0,
|
||
};
|
||
return true;
|
||
}
|
||
|
||
pub fn putRaw(this: *Putter, index: u32, optional_bytes: ?*Data) !bool {
|
||
return this.putImpl(index, optional_bytes, true);
|
||
}
|
||
pub fn put(this: *Putter, index: u32, optional_bytes: ?*Data) !bool {
|
||
return this.putImpl(index, optional_bytes, false);
|
||
}
|
||
};
|
||
};
|
||
|
||
const bun = @import("bun");
|
||
|
||
const JSC = bun.JSC;
|
||
const std = @import("std");
|
||
const JSValue = JSC.JSValue;
|
||
const postgres = @import("./postgres.zig");
|
||
const Data = postgres.Data;
|
||
const types = postgres.types;
|
||
const String = bun.String;
|
||
const int4 = postgres.int4;
|
||
const AnyPostgresError = postgres.AnyPostgresError;
|
||
const protocol = postgres.protocol;
|
||
const PostgresSQLQueryResultMode = postgres.PostgresSQLQueryResultMode;
|
||
const PostgresCachedStructure = postgres.PostgresCachedStructure;
|
||
const debug = postgres.debug;
|
||
const short = postgres.short;
|