Uncomment some of the tests

This commit is contained in:
Jarred Sumner
2023-10-23 02:32:08 -07:00
parent 1a0974693b
commit c2ebd014fb
9 changed files with 3046 additions and 252 deletions

View File

@@ -33,6 +33,14 @@ extern "C" bool BunString__fromJS(JSC::JSGlobalObject* globalObject, JSC::Encode
return bunString->tag != BunStringTag::Dead;
}
extern "C" bool BunString__fromJSRef(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue encodedValue, BunString* bunString)
{
JSC::JSValue value = JSC::JSValue::decode(encodedValue);
*bunString = Bun::toStringRef(globalObject, value);
return bunString->tag != BunStringTag::Dead;
}
extern "C" BunString BunString__createAtom(const char* bytes, size_t length)
{
if (simdutf::validate_ascii(bytes, length)) {

View File

@@ -97,6 +97,7 @@
#include "AsyncContextFrame.h"
#include "JavaScriptCore/InternalFieldTuple.h"
#include "JavaScriptCore/JSDateMath.h"
template<typename UWSResponse>
static void copyToUWS(WebCore::FetchHeaders* headers, UWSResponse* res)
@@ -4716,9 +4717,10 @@ CPP_DECL double JSC__JSValue__getUnixTimestamp(JSC__JSValue timeValue)
return date->internalNumber();
}
extern "C" double WTF__parseDateFromNullTerminatedCharacters(const char* nullTerminatedChars)
extern "C" double Bun__parseDate(JSC::JSGlobalObject* globalObject, BunString* str)
{
return WTF::parseDateFromNullTerminatedCharacters(nullTerminatedChars);
auto& vm = globalObject->vm();
return vm.dateCache.parseDate(globalObject, vm, Bun::toWTFString(*str));
}
extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNullTerminatedString(JSC::JSGlobalObject* globalObject, const char* nullTerminatedChars)

View File

@@ -19,7 +19,7 @@ typedef union DataCellValue {
uint8_t null_value;
WTF::StringImpl* string;
double number;
int32_t integer;
uint32_t integer;
int64_t bigint;
bool boolean;
double date;
@@ -30,17 +30,19 @@ typedef union DataCellValue {
enum class DataCellTag : uint8_t {
Null = 0,
String = 1,
Integer = 2,
Bigint = 3,
Boolean = 4,
Date = 5,
Bytea = 6,
Json = 7,
Double = 2,
Integer = 3,
Bigint = 4,
Boolean = 5,
Date = 6,
Bytea = 7,
Json = 8,
};
typedef struct DataCell {
DataCellTag tag;
DataCellValue value;
bool freeValue;
} DataCell;
static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned count, JSC::JSGlobalObject* globalObject)
@@ -56,9 +58,11 @@ static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned co
break;
case DataCellTag::String: {
object->putDirectOffset(vm, i, jsString(vm, WTF::String(cell.value.string)));
cell.value.string->deref();
break;
}
case DataCellTag::Double:
object->putDirectOffset(vm, i, jsDoubleNumber(cell.value.number));
break;
case DataCellTag::Integer:
object->putDirectOffset(vm, i, jsNumber(cell.value.integer));
break;
@@ -82,8 +86,6 @@ static JSC::JSValue toJS(JSC::Structure* structure, DataCell* cells, unsigned co
case DataCellTag::Json: {
auto str = WTF::String(cell.value.string);
JSC::JSValue json = JSC::JSONParse(globalObject, str);
cell.value.string->deref();
object->putDirectOffset(vm, i, json);
break;
}
@@ -157,13 +159,6 @@ extern "C" EncodedJSValue JSC__createEmptyObjectWithStructure(JSC::JSGlobalObjec
return JSValue::encode(object);
}
extern "C" void JSC__runInDeferralContext(JSC::VM* vm, void* ptr, void (*callback)(void*))
{
GCDeferralContext context(*vm);
JSC::DisallowGC disallowGC;
callback(ptr);
}
extern "C" void JSC__putDirectOffset(JSC::VM* vm, JSC::EncodedJSValue object, unsigned int offset, JSC::EncodedJSValue value)
{
JSValue::decode(object).getObject()->putDirectOffset(*vm, offset, JSValue::decode(value));

View File

@@ -162,7 +162,17 @@ function createConnection({ hostname, port, username, password, tls, query, data
function normalizeStrings(strings) {
if ($isJSArray(strings)) {
return strings.join("?");
const count = strings.length;
if (count === 0) {
return "";
}
var out = strings[0];
for (var i = 1; i < count; i++) {
out += "$" + i;
out += strings[i];
}
return out;
}
return strings + "";

File diff suppressed because one or more lines are too long

View File

@@ -153,6 +153,10 @@ pub const protocol = struct {
pub fn write(this: LengthWriter) anyerror!void {
try this.context.pwrite(&Int32(this.context.offset() - this.index), this.index);
}
pub fn writeExcludingSelf(this: LengthWriter) anyerror!void {
try this.context.pwrite(&Int32(this.context.offset() -| (this.index + 4)), this.index);
}
};
pub inline fn length(this: @This()) anyerror!LengthWriter {
@@ -180,6 +184,10 @@ pub const protocol = struct {
try this.write(std.mem.asBytes(&@byteSwap(@as(u64, @bitCast(value)))));
}
pub fn @"f32"(this: @This(), value: f32) !void {
try this.write(std.mem.asBytes(&@byteSwap(@as(u32, @bitCast(value)))));
}
pub fn short(this: @This(), value: PostgresShort) !void {
try this.write(std.mem.asBytes(&@byteSwap(value)));
}
@@ -674,6 +682,29 @@ pub const protocol = struct {
}
pub const decode = decoderWrap(ErrorResponse, decodeInternal).decode;
pub fn toJS(this: ErrorResponse, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
var b = bun.StringBuilder{};
defer b.deinit(bun.default_allocator);
for (this.messages.items) |msg| {
b.cap += switch (msg) {
inline else => |m| m.utf8ByteLength(),
} + 1;
}
b.allocate(bun.default_allocator) catch {};
for (this.messages.items) |msg| {
var str = switch (msg) {
inline else => |m| m.toUTF8(bun.default_allocator),
};
defer str.deinit();
_ = b.append(str.slice());
_ = b.append("\n");
}
return globalObject.createSyntaxErrorInstance("Postgres error occurred\n{s}", .{b.allocatedSlice()[0..b.len]});
}
};
pub const PortalOrPreparedStatement = union(enum) {
@@ -778,6 +809,8 @@ pub const protocol = struct {
}
};
pub const null_int32 = 4294967295;
pub const DataRow = struct {
pub fn decode(context: anytype, comptime ContextType: type, reader: NewReader(ContextType), comptime forEach: fn (@TypeOf(context), index: u32, bytes: ?*Data) anyerror!bool) anyerror!void {
var remaining_bytes = try reader.length();
@@ -789,6 +822,9 @@ pub const protocol = struct {
const byte_length = try reader.int32();
switch (byte_length) {
0 => break,
null_int32 => {
if (!try forEach(context, @intCast(index), null)) break;
},
else => {
var bytes = try reader.bytes(@intCast(byte_length));
if (!try forEach(context, @intCast(index), &bytes)) break;
@@ -1332,11 +1368,18 @@ pub const types = struct {
number = 0,
json = 114,
boolean = 16,
date = 1184,
datetime = 1114,
timestamptz = 1184,
timestamp = 1114,
time = 1082,
bytea = 17,
bigint = 20,
int64 = 20,
timetz = 1266,
double = 701,
float = 700,
int32 = 23,
/// numeric(precision, decimal), arbitrary precision number
numeric = 1700,
_,
fn toJSWithType(
@@ -1346,7 +1389,7 @@ pub const types = struct {
value: Type,
) anyerror!JSC.JSValue {
switch (tag) {
.number => {
.numeric => {
return number.toJS(globalObject, value);
},
@@ -1358,7 +1401,7 @@ pub const types = struct {
return boolean.toJS(globalObject, value);
},
.date => {
.timestamp, .timestamptz => {
return date.toJS(globalObject, value);
},
@@ -1366,10 +1409,14 @@ pub const types = struct {
return bytea.toJS(globalObject, value);
},
.bigint => {
.int64 => {
return JSC.JSValue.fromInt64NoTruncate(globalObject, value);
},
.int32 => {
return number.toJS(globalObject, value);
},
else => {
return string.toJS(globalObject, value);
},
@@ -1396,7 +1443,7 @@ pub const types = struct {
}
if (tag == .JSDate) {
return .date;
return .timestamp;
}
if (tag.isTypedArray()) {
@@ -1404,7 +1451,7 @@ pub const types = struct {
}
if (tag == .HeapBigInt) {
return .bigint;
return .int64;
}
if (tag.isArrayLike() and value.getLength(globalObject) > 0) {
@@ -1416,8 +1463,12 @@ pub const types = struct {
}
}
if (value.isInt32()) {
return .int32;
}
if (value.isNumber()) {
return .number;
return .double;
}
if (value.isBoolean()) {
@@ -1581,11 +1632,13 @@ pub const PostgresSQLQuery = struct {
status: Status = Status.pending,
is_done: bool = false,
ref_count: u32 = 1,
binary: bool = false,
pub usingnamespace JSC.Codegen.JSPostgresSQLQuery;
pub const Status = enum(u8) {
pending,
written,
running,
success,
fail,
@@ -1640,6 +1693,23 @@ pub const PostgresSQLQuery = struct {
const function = vm.rareData().postgresql_context.onQueryResolveFn.get().?;
globalObject.queueMicrotask(function, &[_]JSC.JSValue{targetValue});
}
pub fn onWriteFail(this: *@This(), err: anyerror, globalObject: *JSC.JSGlobalObject) void {
this.status = .fail;
const thisValue = this.thisValue;
const targetValue = this.target.trySwap() orelse JSC.JSValue.zero;
if (thisValue == .zero or targetValue == .zero) {
return;
}
const instance = globalObject.createTypeErrorInstance("Failed to bind query: {s}", .{@errorName(err)});
// TODO: error handling
var vm = JSC.VirtualMachine.get();
const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?;
globalObject.queueMicrotask(function, &[_]JSC.JSValue{ targetValue, instance });
}
pub fn onError(this: *@This(), err: protocol.ErrorResponse, globalObject: *JSC.JSGlobalObject) void {
this.status = .fail;
defer this.deref();
@@ -1649,31 +1719,11 @@ pub const PostgresSQLQuery = struct {
if (thisValue == .zero or targetValue == .zero) {
return;
}
var b = bun.StringBuilder{};
for (err.messages.items) |msg| {
b.cap += switch (msg) {
inline else => |m| m.utf8ByteLength(),
} + 1;
}
b.allocate(bun.default_allocator) catch {};
for (err.messages.items) |msg| {
var str = switch (msg) {
inline else => |m| m.toUTF8(bun.default_allocator),
};
defer str.deinit();
_ = b.append(str.slice());
_ = b.append("\n");
}
const instance = globalObject.createSyntaxErrorInstance("Postgres error occurred\n{s}", .{b.allocatedSlice()[0..b.len]});
b.deinit(bun.default_allocator);
defer this.deref();
// TODO: error handling
var vm = JSC.VirtualMachine.get();
const function = vm.rareData().postgresql_context.onQueryRejectFn.get().?;
globalObject.queueMicrotask(function, &[_]JSC.JSValue{ targetValue, instance });
globalObject.queueMicrotask(function, &[_]JSC.JSValue{ targetValue, err.toJS(globalObject) });
}
pub fn onSuccess(this: *@This(), _: []const u8, globalObject: *JSC.JSGlobalObject) void {
@@ -1786,39 +1836,68 @@ pub const PostgresSQLQuery = struct {
signature.deinit();
return .zero;
};
if (entry.found_existing) {
this.statement = entry.value_ptr.*;
this.statement.?.ref();
signature.deinit();
PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, PostgresSQLConnection.Writer, writer) catch |err| {
globalObject.throwError(err, "failed to bind and execute query");
const has_params = signature.fields.len > 0;
var did_write = false;
return .zero;
};
} else {
PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| {
globalObject.throwError(err, "failed to prepare and query");
enqueue: {
if (entry.found_existing) {
this.statement = entry.value_ptr.*;
this.statement.?.ref();
signature.deinit();
return .zero;
};
var stmt = bun.default_allocator.create(PostgresSQLStatement) catch |err| {
globalObject.throwError(err, "failed to allocate statement");
return .zero;
};
if (has_params and this.statement.?.status == .parsing) {
// if it has params, we need to wait for ParamDescription to be received before we can write the data
} else {
this.binary = this.statement.?.fields.len > 0;
stmt.* = .{
.signature = signature,
.ref_count = 2,
};
this.statement = stmt;
entry.value_ptr.* = stmt;
PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, PostgresSQLConnection.Writer, writer) catch |err| {
globalObject.throwError(err, "failed to bind and execute query");
return .zero;
};
did_write = true;
}
break :enqueue;
}
// If it does not have params, we can write and execute immediately in one go
if (!has_params) {
PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| {
globalObject.throwError(err, "failed to prepare and query");
signature.deinit();
return .zero;
};
did_write = true;
} else {
PostgresRequest.writeQuery(query_str.slice(), signature.name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| {
globalObject.throwError(err, "failed to write query");
signature.deinit();
return .zero;
};
writer.write(&protocol.Sync) catch |err| {
globalObject.throwError(err, "failed to flush");
signature.deinit();
return .zero;
};
}
{
var stmt = bun.default_allocator.create(PostgresSQLStatement) catch |err| {
globalObject.throwError(err, "failed to allocate statement");
return .zero;
};
stmt.* = .{ .signature = signature, .ref_count = 2, .status = PostgresSQLStatement.Status.parsing };
this.statement = stmt;
entry.value_ptr.* = stmt;
}
}
connection.requests.writeItem(this) catch {};
this.ref();
this.status = .running;
this.status = if (did_write) .running else .pending;
if (connection.is_ready_for_query)
connection.flushData();
@@ -1860,27 +1939,43 @@ pub const PostgresRequest = struct {
var iter = JSC.JSArrayIterator.init(values_array, globalObject);
if (iter.len > 0) {
try writer.short(@intCast(iter.len));
var needs_to_write_format_codes: bool = true;
var consecutive_zero_count: u32 = 0;
while (iter.next()) |value| {
if (value.isUndefinedOrNull()) {
try writer.short(0);
continue;
}
const tag = try types.Tag.fromJS(globalObject, value);
switch (tag) {
.bytea, .number => {
try writer.short(0);
// For now, we only support binary parameters when they're bytea
// This is because postgres needs the exact types in the database
// It's relatively safe to assume that Buffer/TypedArray input is bytea
.bytea => {
if (needs_to_write_format_codes) {
needs_to_write_format_codes = false;
try writer.short(@truncate(iter.len));
}
for (0..consecutive_zero_count) |_| {
try writer.short(0);
}
consecutive_zero_count = 0;
try writer.short(1);
},
else => {
try writer.short(1);
consecutive_zero_count += 1;
},
}
}
try writer.short(@intCast(iter.len));
if (needs_to_write_format_codes) {
try writer.short(0);
} else {
for (0..consecutive_zero_count) |_| {
try writer.short(0);
}
}
try writer.short(@truncate(iter.len));
} else {
try writer.short(0);
try writer.short(0);
@@ -1893,55 +1988,58 @@ pub const PostgresRequest = struct {
while (iter.next()) |value| {
if (value.isUndefinedOrNull()) {
debug(" -> NULL", .{});
try writer.int32(4);
try writer.null();
try writer.int32(@bitCast(@as(i32, -1)));
continue;
}
const tag = try types.Tag.fromJS(globalObject, value);
switch (tag) {
.number => {
debug(" -> {s}", .{@tagName(tag)});
if (value.isInt32()) {
try writer.int32(4);
try writer.int32(value.to(int32));
} else {
try writer.int32(8);
try writer.f64(value.coerceToDouble(globalObject));
}
},
.json => {
debug(" -> {s}", .{@tagName(tag)});
var str = bun.String.empty;
defer str.deref();
value.jsonStringify(globalObject, 0, &str);
try writer.String(str);
const slice = str.toUTF8WithoutRef(bun.default_allocator);
defer slice.deinit();
const l = try writer.length();
try writer.write(slice.slice());
try l.writeExcludingSelf();
},
.boolean => {
debug(" -> {s}", .{@tagName(tag)});
const l = try writer.length();
try writer.boolean(value.toBoolean());
try writer.write(&[_]u8{0});
try l.writeExcludingSelf();
},
.time, .datetime, .date => {
.time, .timestamp, .timestamptz => {
debug(" -> {s}", .{@tagName(tag)});
var buf = std.mem.zeroes([28]u8);
const str = value.toISOString(globalObject, &buf);
try writer.string(str);
const l = try writer.length();
try writer.write(str);
try l.writeExcludingSelf();
},
.bytea => {
var bytes: []const u8 = "";
if (value.asArrayBuffer(globalObject)) |buf| {
bytes = buf.byteSlice();
}
try writer.int32(@intCast(bytes.len));
const l = try writer.length();
debug(" -> {s}: {d}", .{ @tagName(tag), bytes.len });
try writer.bytes(bytes);
try writer.write(bytes);
try l.writeExcludingSelf();
},
else => {
debug(" -> string", .{});
// TODO: check if this leaks
var str = value.toBunString(globalObject);
try writer.String(str);
debug(" -> {s}", .{@tagName(tag)});
const str = String.fromJSRef(value, globalObject);
defer str.deref();
const slice = str.toUTF8WithoutRef(bun.default_allocator);
defer slice.deinit();
const l = try writer.length();
try writer.write(slice.slice());
try l.writeExcludingSelf();
},
}
}
@@ -1949,7 +2047,7 @@ pub const PostgresRequest = struct {
var any_non_text_fields: bool = false;
for (result_fields) |field| {
if (switch (@as(types.Tag, @enumFromInt(field.type_oid))) {
.bigint, .number => true,
.int32, .double, .float, .bytea, .number => true,
else => false,
}) {
any_non_text_fields = true;
@@ -1962,7 +2060,7 @@ pub const PostgresRequest = struct {
for (result_fields) |field| {
try writer.short(
switch (@as(types.Tag, @enumFromInt(field.type_oid))) {
.bigint, .number => 1,
.int32, .double, .float, .bytea, .number => 1,
else => 0,
},
);
@@ -2049,7 +2147,7 @@ pub const PostgresRequest = struct {
comptime Context: type,
writer: protocol.NewWriter(Context),
) !void {
try writeBind(statement.signature.name, bun.String.empty, globalObject, array_value, &.{}, Context, writer);
try writeBind(statement.signature.name, bun.String.empty, globalObject, array_value, statement.fields, Context, writer);
var exec = protocol.Execute{
.p = .{
.prepared_statement = statement.signature.name,
@@ -2245,19 +2343,6 @@ pub const PostgresSQLConnection = struct {
}
pub fn onData(this: *PostgresSQLConnection, data: []const u8) void {
const Deferrer = struct {
data: []const u8,
this: *PostgresSQLConnection,
pub fn run(d: *@This()) callconv(.C) void {
_onData(d.this, d.data);
}
};
var deferrer = Deferrer{ .data = data, .this = this };
this.globalObject.vm().runInDeferralContext(&deferrer, @ptrCast(&Deferrer.run));
}
fn _onData(this: *PostgresSQLConnection, data: []const u8) void {
var vm = this.globalObject.bunVM();
defer vm.drainMicrotasks();
if (this.read_buffer.remaining().len == 0) {
@@ -2267,20 +2352,20 @@ pub const PostgresSQLConnection = struct {
PostgresRequest.onData(this, protocol.StackReader, reader) catch |err| {
if (err == error.ShortRead) {
if (comptime bun.Environment.allow_assert) {
if (@errorReturnTrace()) |trace| {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}\n{}", .{
offset,
consumed,
data.len,
trace,
});
} else {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}", .{
offset,
consumed,
data.len,
});
}
// if (@errorReturnTrace()) |trace| {
// debug("Received short read: last_message_start: {d}, head: {d}, len: {d}\n{}", .{
// offset,
// consumed,
// data.len,
// trace,
// });
// } else {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}", .{
offset,
consumed,
data.len,
});
// }
}
this.read_buffer.head = 0;
@@ -2314,20 +2399,20 @@ pub const PostgresSQLConnection = struct {
}
if (comptime bun.Environment.allow_assert) {
if (@errorReturnTrace()) |trace| {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}\n{}", .{
this.last_message_start,
this.read_buffer.head,
this.read_buffer.byte_list.len,
trace,
});
} else {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}", .{
this.last_message_start,
this.read_buffer.head,
this.read_buffer.byte_list.len,
});
}
// if (@errorReturnTrace()) |trace| {
// debug("Received short read: last_message_start: {d}, head: {d}, len: {d}\n{}", .{
// this.last_message_start,
// this.read_buffer.head,
// this.read_buffer.byte_list.len,
// trace,
// });
// } else {
debug("Received short read: last_message_start: {d}, head: {d}, len: {d}", .{
this.last_message_start,
this.read_buffer.head,
this.read_buffer.byte_list.len,
});
// }
}
return;
@@ -2633,71 +2718,11 @@ pub const PostgresSQLConnection = struct {
};
}
const CellPutter = struct {
object: JSC.JSValue,
vm: *JSC.VM,
globalObject: *JSC.JSGlobalObject,
fields: []const protocol.FieldDescription,
pub fn put(this: *const CellPutter, index: u32, optional_bytes: ?*Data) anyerror!bool {
const putDirectOffset = JSC.JSObject.putDirectOffset;
var bytes_ = optional_bytes orelse {
putDirectOffset(this.object, this.vm, index, JSC.JSValue.jsNull());
return true;
};
defer bytes_.deinit();
const bytes = bytes_.slice();
const object = this.object;
object.ensureStillAlive();
switch (@as(types.Tag, @enumFromInt(this.fields[index].type_oid))) {
.number => {
switch (bytes.len) {
0 => {
putDirectOffset(object, this.vm, index, JSC.JSValue.jsNull());
},
2 => {
putDirectOffset(object, this.vm, index, JSC.JSValue.jsNumber(@as(int32, @as(short, @bitCast(bytes[0..2].*)))));
},
4 => {
putDirectOffset(object, this.vm, index, JSC.JSValue.jsNumber(@as(int32, @bitCast(bytes[0..4].*))));
},
else => {
var eight: usize = 0;
@memcpy(@as(*[8]u8, @ptrCast(&eight))[0..bytes.len], bytes[0..@min(8, bytes.len)]);
eight = @byteSwap(eight);
putDirectOffset(object, this.vm, index, JSC.JSValue.jsNumber(@as(f64, @bitCast(eight))));
},
}
},
.json => {
var str = bun.String.create(bytes);
defer str.deref();
putDirectOffset(object, this.vm, index, str.toJSForParseJSON(this.globalObject));
},
.boolean => {
putDirectOffset(object, this.vm, index, JSC.JSValue.jsBoolean(bytes.len > 0 and bytes[0] == 't'));
},
.time, .datetime, .date => {
putDirectOffset(object, this.vm, index, JSC.JSValue.fromDateString(this.globalObject, bytes_.sliceZ()));
},
.bytea => {
putDirectOffset(object, this.vm, index, JSC.ArrayBuffer.createBuffer(this.globalObject, bytes));
},
else => {
var str = bun.String.create(bytes);
defer str.deref();
putDirectOffset(object, this.vm, index, str.toJS(this.globalObject));
},
}
return true;
}
};
pub const DataCell = extern struct {
tag: Tag,
value: Value,
free_value: bool = false,
pub const Tag = enum(u8) {
null = 0,
@@ -2715,7 +2740,7 @@ pub const PostgresSQLConnection = struct {
null: u8,
string: bun.WTF.StringImpl,
double: f64,
int32: i32,
int32: u32,
int64: i64,
boolean: bool,
date: f64,
@@ -2724,6 +2749,8 @@ pub const PostgresSQLConnection = struct {
};
pub fn deinit(this: *DataCell) void {
if (!this.free_value) return;
switch (this.tag) {
.string => {
this.value.string.deref();
@@ -2731,69 +2758,191 @@ pub const PostgresSQLConnection = struct {
.json => {
this.value.json.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);
},
else => {},
}
}
extern fn WTF__parseDateFromNullTerminatedCharacters([*:0]const u8) f64;
pub const Putter = struct {
list: []DataCell,
fields: []const protocol.FieldDescription,
binary: bool = false,
count: usize = 0,
globalObject: *JSC.JSGlobalObject,
extern fn JSC__constructObjectFromDataCell(*JSC.JSGlobalObject, JSC.JSValue, JSC.JSValue, [*]DataCell, u32) JSC.JSValue;
pub fn toJS(this: *Putter, globalObject: *JSC.JSGlobalObject, array: JSC.JSValue, structure: JSC.JSValue) JSC.JSValue {
return JSC__constructObjectFromDataCell(globalObject, array, structure, this.list.ptr, @truncate(this.fields.len));
}
pub fn parseBinary(comptime tag: types.Tag, comptime ReturnType: type, bytes: []const u8) !ReturnType {
switch (comptime tag) {
.double => {
return @as(f64, @bitCast(try parseBinary(.int64, i64, bytes)));
},
.int64 => {
// pq_getmsgfloat8
if (bytes.len != 8) return error.InvalidBinaryData;
return @byteSwap(@as(i64, @bitCast(bytes[0..8].*)));
},
.int32 => {
// pq_getmsgint
switch (bytes.len) {
1 => {
return @as(u32, @as(u8, @bitCast(bytes[0..1].*)));
},
2 => {
return @as(u32, @byteSwap(@as(u16, @bitCast(bytes[0..2].*))));
},
4 => {
return @byteSwap(@as(u32, @bitCast(bytes[0..4].*)));
},
else => {
return error.UnsupportedIntegerSize;
},
}
},
.float => {
// pq_getmsgfloat4
return @as(f32, @bitCast(try parseBinary(.int32, u32, bytes)));
},
else => @compileError("TODO"),
}
}
pub fn put(this: *Putter, index: u32, optional_bytes: ?*Data) anyerror!bool {
var bytes_ = optional_bytes orelse {
this.list[index] = DataCell{ .tag = .null, .value = .{ .null = 0 } };
this.count += 1;
return true;
};
const bytes = bytes_.slice();
switch (@as(types.Tag, @enumFromInt(this.fields[index].type_oid))) {
.number => {
switch (bytes.len) {
0 => {
this.list[index] = DataCell{ .tag = .null, .value = .{ .null = 0 } };
},
2 => {
this.list[index] = DataCell{ .tag = .int32, .value = .{ .int32 = @as(i32, @as(short, @bitCast(bytes[0..2].*))) } };
},
4 => {
this.list[index] = DataCell{ .tag = .int32, .value = .{ .int32 = @as(i32, @bitCast(bytes[0..4].*)) } };
},
else => {
var eight: i64 = 0;
@memcpy(@as(*[8]u8, @ptrCast(&eight))[0..bytes.len], bytes[0..@min(8, bytes.len)]);
eight = @byteSwap(eight);
this.list[index] = DataCell{ .tag = .int64, .value = .{ .int64 = eight } };
},
const oid = this.fields[index].type_oid;
switch (@as(types.Tag, @enumFromInt(oid))) {
.int32 => {
if (this.binary) {
this.list[index] = DataCell{ .tag = .int32, .value = .{ .int32 = try parseBinary(.int32, u32, bytes) } };
} else {
this.list[index] = DataCell{ .tag = .int32, .value = .{ .int32 = bun.fmt.parseInt(u32, bytes, 0) catch 0 } };
}
},
.double => {
if (this.binary and bytes.len == 8) {
this.list[index] = DataCell{ .tag = .double, .value = .{ .double = try parseBinary(.double, f64, bytes) } };
} else {
const double: f64 = bun.parseDouble(bytes) catch std.math.nan(f64);
this.list[index] = DataCell{ .tag = .double, .value = .{ .double = double } };
}
},
.float => {
if (this.binary and bytes.len == 4) {
this.list[index] = DataCell{ .tag = .double, .value = .{ .double = try parseBinary(.float, f32, bytes) } };
} else {
const float: f64 = bun.parseDouble(bytes) catch std.math.nan(f64);
this.list[index] = DataCell{ .tag = .double, .value = .{ .double = float } };
}
},
.json => {
this.list[index] = DataCell{ .tag = .json, .value = .{ .json = bun.String.create(bytes).value.WTFStringImpl } };
this.list[index] = DataCell{ .tag = .json, .value = .{ .json = bun.String.create(bytes).value.WTFStringImpl }, .free_value = true };
},
.boolean => {
this.list[index] = DataCell{ .tag = .boolean, .value = .{ .boolean = bytes.len > 0 and bytes[0] == 't' } };
},
.time, .datetime, .date => {
this.list[index] = DataCell{ .tag = .date, .value = .{ .date = WTF__parseDateFromNullTerminatedCharacters(bytes_.sliceZ().ptr) } };
.time, .timestamp, .timestamptz => {
var str = bun.String.init(bytes);
defer str.deref();
this.list[index] = DataCell{ .tag = .date, .value = .{ .date = str.parseDate(this.globalObject) } };
},
.bytea => {
this.list[index] = DataCell{ .tag = .bytea, .value = .{ .bytea = .{ @intFromPtr(bytes_.slice().ptr), bytes.len } } };
if (this.binary) {
this.list[index] = DataCell{ .tag = .bytea, .value = .{ .bytea = .{ @intFromPtr(bytes_.slice().ptr), bytes.len } } };
} else {
if (bun.strings.hasPrefixComptime(bytes, "\\x")) {
const hex = bytes[2..];
const len = hex.len / 2;
var buf = try bun.default_allocator.alloc(u8, len);
errdefer bun.default_allocator.free(buf);
this.list[index] = DataCell{
.tag = .bytea,
.value = .{
.bytea = .{
@intFromPtr(buf.ptr),
try bun.strings.decodeHexToBytes(buf, u8, hex),
},
},
.free_value = true,
};
} else {
return error.UnsupportedByteaFormat;
}
}
},
else => {
this.list[index] = DataCell{ .tag = .string, .value = .{ .string = bun.String.create(bytes).value.WTFStringImpl } };
this.list[index] = DataCell{ .tag = .string, .value = .{ .string = bun.String.create(bytes).value.WTFStringImpl }, .free_value = true };
},
}
this.count += 1;
return true;
}
};
};
fn advance(this: *PostgresSQLConnection) !void {
defer this.updateRef();
while (this.requests.readableLength() > 0) {
var req: *PostgresSQLQuery = this.requests.peekItem(0);
switch (req.status) {
.pending => {
var stmt = req.statement orelse return error.ExpectedStatement;
if (stmt.status == .failed) {
req.onError(stmt.error_response, this.globalObject);
this.requests.discard(1);
} else {
break;
}
},
.success, .fail => {
this.requests.discard(1);
req.deref();
},
else => break,
}
}
while (this.requests.readableLength() > 0) {
var req: *PostgresSQLQuery = this.requests.peekItem(0);
var stmt = req.statement orelse return error.ExpectedStatement;
switch (stmt.status) {
.prepared => {
if (req.status == .pending and stmt.status == .prepared) {
const binding_value = PostgresSQLQuery.bindingGetCached(req.thisValue) orelse .zero;
PostgresRequest.bindAndExecute(this.globalObject, stmt, binding_value, PostgresSQLConnection.Writer, this.writer()) catch |err| {
req.onWriteFail(err, this.globalObject);
req.deref();
this.requests.discard(1);
continue;
};
req.status = .running;
req.binary = stmt.fields.len > 0;
} else {
break;
}
},
else => break,
}
}
}
pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.EnumLiteral), comptime Context: type, reader: protocol.NewReader(Context)) !void {
debug("on({s})", .{@tagName(MessageType)});
if (comptime MessageType != .ReadyForQuery) {
@@ -2807,9 +2956,20 @@ pub const PostgresSQLConnection = struct {
var structure = statement.structure(this.js_value, this.globalObject);
std.debug.assert(!structure.isEmptyOrUndefinedOrNull());
var putter = DataCell.Putter{
.list = &.{},
.fields = statement.fields,
.binary = request.binary,
.globalObject = this.globalObject,
};
var stack_buf: [64]DataCell = undefined;
var cells: []DataCell = stack_buf[0..@min(statement.fields.len, stack_buf.len)];
defer {
for (cells[0..putter.count]) |*cell| {
cell.deinit();
}
}
var free_cells = false;
defer if (free_cells) bun.default_allocator.free(cells);
@@ -2817,11 +2977,7 @@ pub const PostgresSQLConnection = struct {
cells = try bun.default_allocator.alloc(DataCell, statement.fields.len);
free_cells = true;
}
var putter = DataCell.Putter{
.list = cells,
.fields = statement.fields,
};
putter.list = cells;
try protocol.DataRow.decode(
&putter,
@@ -2863,6 +3019,8 @@ pub const PostgresSQLConnection = struct {
this.is_ready_for_query = true;
this.socket.setTimeout(300);
try this.advance();
this.flushData();
},
.CommandComplete => {
@@ -2893,6 +3051,9 @@ pub const PostgresSQLConnection = struct {
var request = this.current() orelse return error.ExpectedRequest;
var statement = request.statement orelse return error.ExpectedStatement;
statement.parameters = description.parameters;
if (statement.status == .parsing) {
statement.status = .prepared;
}
},
.RowDescription => {
var description: protocol.RowDescription = undefined;
@@ -2922,10 +3083,43 @@ pub const PostgresSQLConnection = struct {
.ErrorResponse => {
var err: protocol.ErrorResponse = undefined;
try err.decodeInternal(Context, reader);
defer {
err.deinit();
if (this.status == .connecting) {
this.status = .failed;
defer {
err.deinit();
this.poll_ref.unref(this.globalObject.bunVM());
this.updateHasPendingActivity();
}
const on_connect = this.on_connect.swap();
if (on_connect == .zero) return;
const js_value = this.js_value;
js_value.ensureStillAlive();
this.globalObject.queueMicrotask(on_connect, &[_]JSC.JSValue{ err.toJS(this.globalObject), js_value });
// it shouldn't enqueue any requests while connecting
std.debug.assert(this.requests.count == 0);
return;
}
var request = this.current() orelse return error.ExpectedRequest;
var is_error_owned = true;
defer {
if (is_error_owned) {
err.deinit();
}
}
if (request.statement) |stmt| {
if (stmt.status == PostgresSQLStatement.Status.parsing) {
stmt.status = PostgresSQLStatement.Status.failed;
stmt.error_response = err;
is_error_owned = false;
if (this.statements.remove(bun.hash(stmt.signature.name))) {
stmt.deref();
}
}
}
_ = this.requests.discard(1);
this.updateRef();
@@ -3010,6 +3204,14 @@ pub const PostgresSQLStatement = struct {
fields: []const protocol.FieldDescription = &[_]protocol.FieldDescription{},
parameters: []const int32 = &[_]int32{},
signature: Signature,
status: Status = Status.parsing,
error_response: protocol.ErrorResponse = .{},
pub const Status = enum {
parsing,
prepared,
failed,
};
pub fn ref(this: *@This()) void {
std.debug.assert(this.ref_count > 0);
this.ref_count += 1;
@@ -3035,6 +3237,7 @@ pub const PostgresSQLStatement = struct {
bun.default_allocator.free(this.fields);
bun.default_allocator.free(this.parameters);
this.cached_structure.deinit();
this.error_response.deinit();
this.signature.deinit();
bun.default_allocator.destroy(this);
}
@@ -3076,7 +3279,6 @@ const Signature = struct {
pub fn hash(this: *const Signature) u64 {
var hasher = std.hash.Wyhash.init(0);
defer hasher.deinit();
hasher.update(this.name);
hasher.update(std.mem.sliceAsBytes(this.fields));
return hasher.final();
@@ -3103,16 +3305,16 @@ const Signature = struct {
}
const tag = try types.Tag.fromJS(globalObject, value);
try fields.append(@byteSwap(@intFromEnum(tag)));
try fields.append(0);
switch (tag) {
.number => try name.appendSlice(".number"),
.json => try name.appendSlice(".json"),
.boolean => try name.appendSlice(".boolean"),
.date => try name.appendSlice(".date"),
.datetime => try name.appendSlice(".datetime"),
.timestamp => try name.appendSlice(".timestamp"),
.timestamptz => try name.appendSlice(".timestamptz"),
.time => try name.appendSlice(".time"),
.bytea => try name.appendSlice(".bytea"),
.bigint => try name.appendSlice(".bigint"),
.int64 => try name.appendSlice(".int64"),
else => try name.appendSlice(".string"),
}
}

View File

@@ -487,6 +487,17 @@ pub const String = extern struct {
}
}
pub fn fromJSRef(value: bun.JSC.JSValue, globalObject: *JSC.JSGlobalObject) String {
JSC.markBinding(@src());
var out: String = String.dead;
if (BunString__fromJSRef(globalObject, value, &out)) {
return out;
} else {
return String.dead;
}
}
pub fn tryFromJS(value: bun.JSC.JSValue, globalObject: *JSC.JSGlobalObject) ?String {
JSC.markBinding(@src());
@@ -690,11 +701,18 @@ pub const String = extern struct {
};
}
extern fn Bun__parseDate(*JSC.JSGlobalObject, *String) f64;
extern fn BunString__fromJS(globalObject: *JSC.JSGlobalObject, value: bun.JSC.JSValue, out: *String) bool;
extern fn BunString__fromJSRef(globalObject: *JSC.JSGlobalObject, value: bun.JSC.JSValue, out: *String) bool;
extern fn BunString__toJS(globalObject: *JSC.JSGlobalObject, in: *String) JSC.JSValue;
extern fn BunString__toJSWithLength(globalObject: *JSC.JSGlobalObject, in: *String, usize) JSC.JSValue;
extern fn BunString__toWTFString(this: *String) void;
pub fn parseDate(this: *String, globalObject: *JSC.JSGlobalObject) f64 {
JSC.markBinding(@src());
return Bun__parseDate(globalObject, this);
}
pub fn ref(this: String) void {
switch (this.tag) {
.WTFStringImpl => this.value.WTFStringImpl.ref(),

34
test/js/sql/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,34 @@
import { spawnSync } from "child_process";
exec("dropdb", ["bun_sql_test"]);
// Needs to have a server.crt file https://www.postgresql.org/docs/current/ssl-tcp.html#SSL-CERTIFICATE-CREATION
// exec('psql', ['-c', 'alter system set ssl=on'])
exec("psql", ["-c", "drop user bun_sql_test"]);
exec("psql", ["-c", "create user bun_sql_test"]);
exec("psql", ["-c", "alter system set password_encryption=md5"]);
exec("psql", ["-c", "select pg_reload_conf()"]);
exec("psql", ["-c", "drop user if exists bun_sql_test_md5"]);
exec("psql", ["-c", "create user bun_sql_test_md5 with password 'bun_sql_test_md5'"]);
exec("psql", ["-c", "alter system set password_encryption='scram-sha-256'"]);
exec("psql", ["-c", "select pg_reload_conf()"]);
exec("psql", ["-c", "drop user if exists bun_sql_test_scram"]);
exec("psql", ["-c", "create user bun_sql_test_scram with password 'bun_sql_test_scram'"]);
exec("createdb", ["bun_sql_test"]);
exec("psql", ["-c", "grant all on database bun_sql_test to bun_sql_test"]);
exec("psql", ["-c", "alter database bun_sql_test owner to bun_sql_test"]);
export function exec(cmd, args) {
const { stderr } = spawnSync(cmd, args, { stdio: "pipe", encoding: "utf8" });
if (stderr && !stderr.includes("already exists") && !stderr.includes("does not exist")) throw stderr;
}
async function execAsync(cmd, args) {
// eslint-disable-line
let stderr = "";
const cp = await spawn(cmd, args, { stdio: "pipe", encoding: "utf8" }); // eslint-disable-line
cp.stderr.on("data", x => (stderr += x));
await new Promise(x => cp.on("exit", x));
if (stderr && !stderr.includes("already exists") && !stderr.includes("does not exist")) throw new Error(stderr);
}

2525
test/js/sql/sql.test.ts Normal file

File diff suppressed because it is too large Load Diff