mirror of
https://github.com/oven-sh/bun
synced 2026-02-18 23:01:58 +00:00
## What does this PR do? Currently binary columns are returned as strings which means they get corrupted when encoded in UTF8. This PR returns binary columns as Buffers which is what user's actually expect and is also consistent with PostgreSQL and SQLite. ### How did you verify your code works? I added tests to verify the correct behavior. Before there were no tests for binary columns at all. This fixes #23991
271 lines
12 KiB
Zig
271 lines
12 KiB
Zig
pub const Header = @import("./ResultSetHeader.zig");
|
|
|
|
pub const Row = struct {
|
|
values: []SQLDataCell = &[_]SQLDataCell{},
|
|
columns: []const ColumnDefinition41,
|
|
binary: bool = false,
|
|
raw: bool = false,
|
|
bigint: bool = false,
|
|
globalObject: *jsc.JSGlobalObject,
|
|
|
|
pub fn toJS(this: *Row, globalObject: *jsc.JSGlobalObject, array: JSValue, structure: JSValue, flags: SQLDataCell.Flags, result_mode: SQLQueryResultMode, cached_structure: ?CachedStructure) !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 SQLDataCell.constructObjectFromDataCell(
|
|
globalObject,
|
|
array,
|
|
structure,
|
|
this.values.ptr,
|
|
@truncate(this.values.len),
|
|
flags,
|
|
@intFromEnum(result_mode),
|
|
names,
|
|
names_count,
|
|
);
|
|
}
|
|
|
|
pub fn deinit(this: *Row, allocator: std.mem.Allocator) void {
|
|
for (this.values) |*value| {
|
|
value.deinit();
|
|
}
|
|
allocator.free(this.values);
|
|
|
|
// this.columns is intentionally left out.
|
|
}
|
|
|
|
pub fn decodeInternal(this: *Row, allocator: std.mem.Allocator, comptime Context: type, reader: NewReader(Context)) AnyMySQLError.Error!void {
|
|
if (this.binary) {
|
|
try this.decodeBinary(allocator, Context, reader);
|
|
} else {
|
|
try this.decodeText(allocator, Context, reader);
|
|
}
|
|
}
|
|
|
|
fn parseValueAndSetCell(this: *Row, cell: *SQLDataCell, column: *const ColumnDefinition41, value: *const Data) void {
|
|
debug("parseValueAndSetCell: {s} {s}", .{ @tagName(column.column_type), value.slice() });
|
|
return switch (column.column_type) {
|
|
.MYSQL_TYPE_FLOAT, .MYSQL_TYPE_DOUBLE => {
|
|
const val: f64 = bun.parseDouble(value.slice()) catch std.math.nan(f64);
|
|
cell.* = SQLDataCell{ .tag = .float8, .value = .{ .float8 = val } };
|
|
},
|
|
.MYSQL_TYPE_TINY, .MYSQL_TYPE_SHORT => {
|
|
if (column.flags.UNSIGNED) {
|
|
const val: u16 = std.fmt.parseInt(u16, value.slice(), 10) catch 0;
|
|
cell.* = SQLDataCell{ .tag = .uint4, .value = .{ .uint4 = val } };
|
|
} else {
|
|
const val: i16 = std.fmt.parseInt(i16, value.slice(), 10) catch 0;
|
|
cell.* = SQLDataCell{ .tag = .int4, .value = .{ .int4 = val } };
|
|
}
|
|
},
|
|
.MYSQL_TYPE_LONG => {
|
|
if (column.flags.UNSIGNED) {
|
|
const val: u32 = std.fmt.parseInt(u32, value.slice(), 10) catch 0;
|
|
cell.* = SQLDataCell{ .tag = .uint4, .value = .{ .uint4 = val } };
|
|
} else {
|
|
const val: i32 = std.fmt.parseInt(i32, value.slice(), 10) catch std.math.minInt(i32);
|
|
cell.* = SQLDataCell{ .tag = .int4, .value = .{ .int4 = val } };
|
|
}
|
|
},
|
|
.MYSQL_TYPE_INT24 => {
|
|
if (column.flags.UNSIGNED) {
|
|
const val: u24 = std.fmt.parseInt(u24, value.slice(), 10) catch 0;
|
|
cell.* = SQLDataCell{ .tag = .uint4, .value = .{ .uint4 = val } };
|
|
} else {
|
|
const val: i24 = std.fmt.parseInt(i24, value.slice(), 10) catch std.math.minInt(i24);
|
|
cell.* = SQLDataCell{ .tag = .int4, .value = .{ .int4 = val } };
|
|
}
|
|
},
|
|
.MYSQL_TYPE_LONGLONG => {
|
|
if (column.flags.UNSIGNED) {
|
|
const val: u64 = std.fmt.parseInt(u64, value.slice(), 10) catch 0;
|
|
if (val <= std.math.maxInt(u32)) {
|
|
cell.* = SQLDataCell{ .tag = .uint4, .value = .{ .uint4 = @intCast(val) } };
|
|
return;
|
|
}
|
|
if (this.bigint) {
|
|
cell.* = SQLDataCell{ .tag = .uint8, .value = .{ .uint8 = val } };
|
|
return;
|
|
}
|
|
} else {
|
|
const val: i64 = std.fmt.parseInt(i64, value.slice(), 10) catch 0;
|
|
if (val >= std.math.minInt(i32) and val <= std.math.maxInt(i32)) {
|
|
cell.* = SQLDataCell{ .tag = .int4, .value = .{ .int4 = @intCast(val) } };
|
|
return;
|
|
}
|
|
if (this.bigint) {
|
|
cell.* = SQLDataCell{ .tag = .int8, .value = .{ .int8 = val } };
|
|
return;
|
|
}
|
|
}
|
|
|
|
const slice = value.slice();
|
|
cell.* = SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 };
|
|
},
|
|
.MYSQL_TYPE_JSON => {
|
|
const slice = value.slice();
|
|
cell.* = SQLDataCell{ .tag = .json, .value = .{ .json = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 };
|
|
},
|
|
|
|
.MYSQL_TYPE_TIME => {
|
|
// lets handle TIME special case as string
|
|
// -838:59:50 to 838:59:59 is valid
|
|
const slice = value.slice();
|
|
cell.* = SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 };
|
|
},
|
|
.MYSQL_TYPE_DATE, .MYSQL_TYPE_DATETIME, .MYSQL_TYPE_TIMESTAMP => {
|
|
var str = bun.String.init(value.slice());
|
|
defer str.deref();
|
|
const date = brk: {
|
|
break :brk str.parseDate(this.globalObject) catch |err| {
|
|
_ = this.globalObject.takeException(err);
|
|
break :brk std.math.nan(f64);
|
|
};
|
|
};
|
|
cell.* = SQLDataCell{ .tag = .date, .value = .{ .date = date } };
|
|
},
|
|
.MYSQL_TYPE_BIT => {
|
|
// BIT(1) is a special case, it's a boolean
|
|
if (column.column_length == 1) {
|
|
const slice = value.slice();
|
|
cell.* = SQLDataCell{ .tag = .bool, .value = .{ .bool = if (slice.len > 0 and slice[0] == 1) 1 else 0 } };
|
|
} else {
|
|
cell.* = SQLDataCell.raw(value);
|
|
}
|
|
},
|
|
else => {
|
|
if (column.flags.BINARY) {
|
|
cell.* = SQLDataCell.raw(value);
|
|
} else {
|
|
const slice = value.slice();
|
|
cell.* = SQLDataCell{ .tag = .string, .value = .{ .string = if (slice.len > 0) bun.String.cloneUTF8(slice).value.WTFStringImpl else null }, .free_value = 1 };
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
fn decodeText(this: *Row, allocator: std.mem.Allocator, comptime Context: type, reader: NewReader(Context)) AnyMySQLError.Error!void {
|
|
const cells = try allocator.alloc(SQLDataCell, this.columns.len);
|
|
@memset(cells, SQLDataCell{ .tag = .null, .value = .{ .null = 0 } });
|
|
errdefer {
|
|
for (cells) |*value| {
|
|
value.deinit();
|
|
}
|
|
allocator.free(cells);
|
|
}
|
|
|
|
for (cells, 0..) |*value, index| {
|
|
if (decodeLengthInt(reader.peek())) |result| {
|
|
const column = this.columns[index];
|
|
if (result.value == 0xfb) {
|
|
// 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 = .null, .value = .{ .null = 0 } };
|
|
} else {
|
|
if (this.raw) {
|
|
var data = try reader.rawEncodeLenData();
|
|
defer data.deinit();
|
|
value.* = SQLDataCell.raw(&data);
|
|
} else {
|
|
reader.skip(result.bytes_read);
|
|
var string_data = try reader.read(@intCast(result.value));
|
|
defer string_data.deinit();
|
|
this.parseValueAndSetCell(value, &column, &string_data);
|
|
}
|
|
}
|
|
value.index = switch (column.name_or_index) {
|
|
// The indexed columns can be out of order.
|
|
.index => |i| i,
|
|
|
|
else => @intCast(index),
|
|
};
|
|
value.isIndexedColumn = switch (column.name_or_index) {
|
|
.duplicate => 2,
|
|
.index => 1,
|
|
.name => 0,
|
|
};
|
|
} else {
|
|
return error.InvalidResultRow;
|
|
}
|
|
}
|
|
|
|
this.values = cells;
|
|
}
|
|
|
|
fn decodeBinary(this: *Row, allocator: std.mem.Allocator, comptime Context: type, reader: NewReader(Context)) AnyMySQLError.Error!void {
|
|
// Header
|
|
_ = try reader.int(u8);
|
|
|
|
// Null bitmap
|
|
const bitmap_bytes = (this.columns.len + 7 + 2) / 8;
|
|
var null_bitmap = try reader.read(bitmap_bytes);
|
|
defer null_bitmap.deinit();
|
|
|
|
const cells = try allocator.alloc(SQLDataCell, this.columns.len);
|
|
@memset(cells, SQLDataCell{ .tag = .null, .value = .{ .null = 0 } });
|
|
errdefer {
|
|
for (cells) |*value| {
|
|
value.deinit();
|
|
}
|
|
allocator.free(cells);
|
|
}
|
|
// Skip first 2 bits of null bitmap (reserved)
|
|
const bitmap_offset: usize = 2;
|
|
|
|
for (cells, 0..) |*value, i| {
|
|
const byte_pos = (bitmap_offset + i) >> 3;
|
|
const bit_pos = @as(u3, @truncate((bitmap_offset + i) & 7));
|
|
const is_null = (null_bitmap.slice()[byte_pos] & (@as(u8, 1) << bit_pos)) != 0;
|
|
|
|
if (is_null) {
|
|
value.* = SQLDataCell{ .tag = .null, .value = .{ .null = 0 } };
|
|
continue;
|
|
}
|
|
|
|
const column = this.columns[i];
|
|
value.* = try decodeBinaryValue(this.globalObject, column.column_type, column.column_length, this.raw, this.bigint, column.flags.UNSIGNED, column.flags.BINARY, Context, reader);
|
|
value.index = switch (column.name_or_index) {
|
|
// The indexed columns can be out of order.
|
|
.index => |idx| idx,
|
|
|
|
else => @intCast(i),
|
|
};
|
|
value.isIndexedColumn = switch (column.name_or_index) {
|
|
.duplicate => 2,
|
|
.index => 1,
|
|
.name => 0,
|
|
};
|
|
}
|
|
|
|
this.values = cells;
|
|
}
|
|
|
|
pub const decode = decoderWrap(Row, decodeInternal).decodeAllocator;
|
|
};
|
|
|
|
const debug = bun.Output.scoped(.MySQLResultSet, .visible);
|
|
|
|
const AnyMySQLError = @import("./AnyMySQLError.zig");
|
|
const CachedStructure = @import("../../shared/CachedStructure.zig");
|
|
const ColumnDefinition41 = @import("./ColumnDefinition41.zig");
|
|
const bun = @import("bun");
|
|
const std = @import("std");
|
|
const Data = @import("../../shared/Data.zig").Data;
|
|
const SQLDataCell = @import("../../shared/SQLDataCell.zig").SQLDataCell;
|
|
const SQLQueryResultMode = @import("../../shared/SQLQueryResultMode.zig").SQLQueryResultMode;
|
|
const decodeBinaryValue = @import("./DecodeBinaryValue.zig").decodeBinaryValue;
|
|
const decodeLengthInt = @import("./EncodeInt.zig").decodeLengthInt;
|
|
|
|
const NewReader = @import("./NewReader.zig").NewReader;
|
|
const decoderWrap = @import("./NewReader.zig").decoderWrap;
|
|
|
|
const jsc = bun.jsc;
|
|
const JSValue = jsc.JSValue;
|